Q40: tasks choose a writing mode; house style applies to conversational work only
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
645ce686bb
commit
7be072f8c5
18 files changed
+119
-41
No files matched your search
@@ -0,0 +1,7 @@
|
||||
export const taskWritingModeSchema = `
|
||||
alter table tasks add column writing_mode text not null default 'conversational'
|
||||
check (writing_mode in ('conversational', 'technical', 'structured'));
|
||||
|
||||
insert into schema_migrations(version, name, applied_at)
|
||||
values (66, 'task_writing_mode', datetime('now'));
|
||||
`;
|
||||
@@ -64,6 +64,7 @@ import { providerContextWindowSchema } from "./migrations/062-provider-context-w
|
||||
import { knowledgeScopeSchema } from "./migrations/063-knowledge-scope";
|
||||
import { knowledgeRevisionsSchema } from "./migrations/064-knowledge-revisions";
|
||||
import { documentVersionsSchema } from "./migrations/065-document-versions";
|
||||
import { taskWritingModeSchema } from "./migrations/066-task-writing-mode";
|
||||
|
||||
interface Migration {
|
||||
version: number;
|
||||
@@ -137,6 +138,7 @@ const MIGRATIONS: Migration[] = [
|
||||
{ version: 63, name: "knowledge_scope", sql: knowledgeScopeSchema },
|
||||
{ version: 64, name: "knowledge_revisions", sql: knowledgeRevisionsSchema },
|
||||
{ version: 65, name: "document_versions", sql: documentVersionsSchema },
|
||||
{ version: 66, name: "task_writing_mode", sql: taskWritingModeSchema },
|
||||
];
|
||||
|
||||
const SCHEMA_VERSION = MIGRATIONS[MIGRATIONS.length - 1].version;
|
||||
|
||||
@@ -30,7 +30,7 @@ export function listSkillsTool(deps: AgentToolDeps, ctx: AgentToolContext): stri
|
||||
const summary = skillSummary(skill.manifest_json);
|
||||
if (forced.has(skill.slug)) {
|
||||
const language = skill.slug === "humanizer-zh" ? "Chinese" : "English";
|
||||
return `- ${skill.name} (${skill.slug})${summary}: house style — carried automatically by every agent when the app language is ${language}`;
|
||||
return `- ${skill.name} (${skill.slug})${summary}: house style — carried automatically by every agent on conversational tasks when the app language is ${language}`;
|
||||
}
|
||||
const holders = bindings
|
||||
.filter((binding) => binding.skill_id === skill.id)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, type RiskLevel } from "../../shared/domain";
|
||||
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, WRITING_MODE, type RiskLevel, type WritingMode } from "../../shared/domain";
|
||||
import { ORGANIZATION_DEPARTMENTS } from "./org-governance-service";
|
||||
import type { OrganizationService } from "./organization-service";
|
||||
import { buildTaskHandoff, HANDOFF_TOOL_PROPERTIES } from "./agent-task-handoff";
|
||||
@@ -15,7 +15,7 @@ import { KNOWLEDGE_TOOLS, recordKnowledgeTool, reviseKnowledgeTool, searchKnowle
|
||||
import { DOCUMENT_TOOLS, DOCUMENT_TYPES, listDocumentsTool, readDocumentTool, updateDocumentTool } from "./agent-document-tools";
|
||||
import { sendMessageTool } from "./agent-send-message";
|
||||
import { withSavepoint } from "../database/connection";
|
||||
import { optionalString, optionalStringArray, requireString } from "./agent-tool-args";
|
||||
import { optionalEnum, optionalString, optionalStringArray, requireString } from "./agent-tool-args";
|
||||
import type { JsonRecord } from "./provider-http";
|
||||
import type { ToolCall, ToolDefinition } from "./provider-tool-turn";
|
||||
|
||||
@@ -85,6 +85,11 @@ export const AGENT_TOOLS: ToolDefinition[] = [
|
||||
type: "boolean",
|
||||
description: "Set true for engineering work that must change code or run commands (build, test) in the workspace. The assignee then gets run_command; the operator approves such tasks before they execute.",
|
||||
},
|
||||
writing_mode: {
|
||||
type: "string",
|
||||
enum: ["conversational", "technical", "structured"],
|
||||
description: "How the output reads. conversational (default): plain human prose in house style. technical: specs, API docs, code reviews; precise, no house style. structured: output must match an exact format (JSON, table, template).",
|
||||
},
|
||||
...HANDOFF_TOOL_PROPERTIES,
|
||||
},
|
||||
required: ["title", "description"],
|
||||
@@ -337,6 +342,7 @@ function createTask(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord
|
||||
const assignee = assigneeName ? requireRunnableAgentByName(deps.db, ctx.orgId, assigneeName) : null;
|
||||
const handoff = buildTaskHandoff(deps.db, ctx.orgId, { taskId: ctx.taskId, threadId: ctx.sourceThreadId }, args);
|
||||
const parentTaskId = delegationParent(deps.db, ctx);
|
||||
const writingMode = optionalEnum(args.writing_mode, "writing_mode", Object.values(WRITING_MODE), WRITING_MODE.CONVERSATIONAL) as WritingMode;
|
||||
const create = () => {
|
||||
const taskId = deps.organization.createTask({
|
||||
orgId: ctx.orgId,
|
||||
@@ -350,6 +356,7 @@ function createTask(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord
|
||||
workspaceId: ctx.workspace?.id ?? null,
|
||||
priority: TASK_PRIORITY.NORMAL,
|
||||
requiresFilesystem: args.requires_filesystem === true,
|
||||
writingMode,
|
||||
});
|
||||
deps.db.prepare("update tasks set handoff_json = ?, parent_task_id = ? where id = ?")
|
||||
.run(JSON.stringify(handoff), parentTaskId, taskId);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import { APPROVAL_STATE, RISK_LEVEL, TASK_STATUS, type ApprovalState } from "../../shared/domain";
|
||||
import { APPROVAL_STATE, RISK_LEVEL, TASK_STATUS, WRITING_MODE, type ApprovalState } from "../../shared/domain";
|
||||
import { createId } from "../../shared/ids";
|
||||
import type { CreateAgentInput, CreateGoalInput, CreateOrganizationInput, CreateProjectInput, CreateTaskInput, CreateWorkspaceInput, DeleteTaskInput, UpdateAgentInput, UpdateEmployeeProfileInput, UpdateTaskInput } from "../../shared/commands";
|
||||
import { AuditService } from "./audit-service";
|
||||
@@ -234,9 +234,9 @@ export class OrganizationService {
|
||||
this.db.prepare(`
|
||||
insert into tasks(
|
||||
id, org_id, title, description, status, assignee_agent_id, risk,
|
||||
project_id, goal_id, workspace_id, priority, requires_filesystem, origin, created_at, updated_at
|
||||
project_id, goal_id, workspace_id, priority, requires_filesystem, writing_mode, origin, created_at, updated_at
|
||||
)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`).run(
|
||||
id,
|
||||
input.orgId,
|
||||
@@ -250,6 +250,7 @@ export class OrganizationService {
|
||||
input.workspaceId,
|
||||
input.priority,
|
||||
input.requiresFilesystem ? 1 : 0,
|
||||
input.writingMode ?? WRITING_MODE.CONVERSATIONAL,
|
||||
input.origin ?? null,
|
||||
now,
|
||||
now,
|
||||
|
||||
@@ -244,8 +244,8 @@ export class SkillService {
|
||||
* agent carries the forced house-style skill once the catalogue holds a
|
||||
* language variant, plus whatever bindings the CEO granted.
|
||||
*/
|
||||
hasRuntimeSkills(orgId: string, agentId: string): boolean {
|
||||
if (this.forcedSkillRows(orgId).length > 0) return true;
|
||||
hasRuntimeSkills(orgId: string, agentId: string, houseStyle: boolean): boolean {
|
||||
if (houseStyle && this.forcedSkillRows(orgId).length > 0) return true;
|
||||
return Boolean(this.db.prepare(`
|
||||
select sb.id from skill_bindings sb join skill_definitions sd on sd.id = sb.skill_id and sd.status = 'active'
|
||||
where sb.org_id = ? and sb.agent_id = ? and sb.enabled = 1 limit 1
|
||||
@@ -294,7 +294,7 @@ export class SkillService {
|
||||
* prompt instead of silently overflowing the model's context.
|
||||
*/
|
||||
resolveRuntimeSnapshot(input: ResolveAgentSkillsInput): string {
|
||||
const forcedRows = this.forcedSkillRows(input.orgId);
|
||||
const forcedRows = input.houseStyle ? this.forcedSkillRows(input.orgId) : [];
|
||||
const boundRows = this.db.prepare(`
|
||||
select
|
||||
sb.skill_id as skill_id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { DatabaseSync } from "node:sqlite";
|
||||
import type { RunTaskInput } from "../../shared/commands";
|
||||
import { TASK_STATUS } from "../../shared/domain";
|
||||
import { TASK_STATUS, WRITING_MODE } from "../../shared/domain";
|
||||
import { isTransientProviderFailure } from "../../shared/provider-runtime-domain";
|
||||
import { createId } from "../../shared/ids";
|
||||
import { AuditService } from "./audit-service";
|
||||
@@ -148,7 +148,7 @@ export class TaskExecutionService {
|
||||
let providerRequestId: string | null = null;
|
||||
let skillRuntimeSnapshot: SkillRuntimeSnapshotRow | null = null;
|
||||
try {
|
||||
skillRuntimeSnapshot = this.resolveSkillRuntimeSnapshot(orgId, agent.id, runId, skillBudgetTokens(provider.context_window_tokens), `${task.title}\n${task.description}`, { providerKey: provider.provider_key, model: provider.model });
|
||||
skillRuntimeSnapshot = this.resolveSkillRuntimeSnapshot(orgId, agent.id, runId, skillBudgetTokens(provider.context_window_tokens), `${task.title}\n${task.description}`, { providerKey: provider.provider_key, model: provider.model }, task.writing_mode === WRITING_MODE.CONVERSATIONAL);
|
||||
// Watermark for reply delivery and mid-run message polling.
|
||||
const runStartedAt = new Date().toISOString();
|
||||
const chatContext = loadChatWakeContext(this.db, orgId, agent.id, isolation.secretScopeJson);
|
||||
@@ -391,9 +391,9 @@ export class TaskExecutionService {
|
||||
`).run(TASK_RUN_STATE.FAILED, providerRequestId, new Date().toISOString(), errorMessage, runId, orgId);
|
||||
}
|
||||
|
||||
private resolveSkillRuntimeSnapshot(orgId: string, agentId: string, runId: string, maxTokens: number, taskText: string, runModel: RunModel): SkillRuntimeSnapshotRow | null {
|
||||
if (!this.skills.hasRuntimeSkills(orgId, agentId)) return null;
|
||||
const snapshotId = this.skills.resolveRuntimeSnapshot({ orgId, agentId, runId, maxTokens, taskText, runModel });
|
||||
private resolveSkillRuntimeSnapshot(orgId: string, agentId: string, runId: string, maxTokens: number, taskText: string, runModel: RunModel, houseStyle: boolean): SkillRuntimeSnapshotRow | null {
|
||||
if (!this.skills.hasRuntimeSkills(orgId, agentId, houseStyle)) return null;
|
||||
const snapshotId = this.skills.resolveRuntimeSnapshot({ orgId, agentId, runId, maxTokens, taskText, runModel, houseStyle });
|
||||
const snapshot = this.db.prepare(`
|
||||
select id, compiled_prompt, tools_manifest, source_revision_ids
|
||||
from skill_runtime_snapshots
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { WRITING_MODE, type WritingMode } from "../../shared/domain";
|
||||
import { canonicalMentionHandle } from "../../shared/mention-handles";
|
||||
|
||||
const RECORD_LEARNINGS_INSTRUCTION =
|
||||
@@ -16,6 +17,7 @@ export interface TaskExecutionTaskRow {
|
||||
workspace_id: string | null;
|
||||
priority: string;
|
||||
requires_filesystem: number;
|
||||
writing_mode: WritingMode;
|
||||
handoff_json: string | null;
|
||||
origin: string | null;
|
||||
}
|
||||
@@ -132,7 +134,7 @@ function capabilityBriefing(input: AgentInstructionContext): string {
|
||||
);
|
||||
if (input.isCeo) {
|
||||
lines.push(
|
||||
"You are the CEO: you automatically carry every connected service, and access for everyone else is yours to manage — when you delegate work that needs an external service, grant it first with grant_connector and take it back with revoke_connector when done. The company also ships a built-in skill library: staff every hire with the skills their role needs by passing skills to hire_agent (several per person is normal), and adjust later with grant_skill / revoke_skill. You run only with skills granted to you too: grant yourself a playbook before you rely on it. A teammate without the grant cannot use the tool or skill, so grant before you assign. The humanizer skill is company house style — every agent, you included, carries it automatically and must write like a person, not like an AI.",
|
||||
"You are the CEO: you automatically carry every connected service, and access for everyone else is yours to manage — when you delegate work that needs an external service, grant it first with grant_connector and take it back with revoke_connector when done. The company also ships a built-in skill library: staff every hire with the skills their role needs by passing skills to hire_agent (several per person is normal), and adjust later with grant_skill / revoke_skill. You run only with skills granted to you too: grant yourself a playbook before you rely on it. A teammate without the grant cannot use the tool or skill, so grant before you assign. The humanizer skill is company house style — every agent, you included, carries it on conversational work and must write like a person, not like an AI. For specs and API docs pass writing_mode technical to create_task; for output in an exact format pass structured; both skip house style.",
|
||||
);
|
||||
} else if (input.connectorNames.length === 0) {
|
||||
lines.push(
|
||||
@@ -160,6 +162,17 @@ function roleVoice(input: AgentInstructionContext): string {
|
||||
return "You own your craft. Ask the question early instead of guessing, do the work properly, and when you are stuck for more than a few minutes say so in the thread instead of spinning in place.";
|
||||
}
|
||||
|
||||
/** The writing rule for technical and structured tasks, which run without house style. */
|
||||
export function writingModeLines(mode: WritingMode): string[] {
|
||||
if (mode === WRITING_MODE.TECHNICAL) {
|
||||
return ["", "Technical writing: this task's output is a spec or technical document. Be exact and complete: define terms, name types, units and edge cases, prefer lists and tables over prose, and never trade precision for tone."];
|
||||
}
|
||||
if (mode === WRITING_MODE.STRUCTURED) {
|
||||
return ["", "Structured output: the deliverable must match the requested format exactly (JSON, table, template). No prose around it, no extra fields, no commentary inside it."];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function skillToolLines(toolsManifest: string, available: string[]): string[] {
|
||||
const { requiredTools } = JSON.parse(toolsManifest) as { requiredTools: string[] };
|
||||
const missing = requiredTools.filter((tool) => !available.includes(tool));
|
||||
@@ -216,6 +229,7 @@ export function buildTaskPrompt(input: {
|
||||
`Mounts: ${input.isolation.workspaceMountJson}`,
|
||||
);
|
||||
}
|
||||
lines.push(...writingModeLines(input.task.writing_mode));
|
||||
lines.push(...input.handoffLines);
|
||||
lines.push(...input.knowledgeLines);
|
||||
lines.push("", `Secret scope: ${input.isolation.secretScopeJson}`);
|
||||
|
||||
@@ -28,6 +28,10 @@ export const zh: Record<string, string> = {
|
||||
"Advanced": "高级",
|
||||
"Integrations": "集成密钥",
|
||||
"Document": "文档",
|
||||
"Writing": "写作方式",
|
||||
"Conversational (house style)": "日常沟通(公司文风)",
|
||||
"Technical spec": "技术规格",
|
||||
"Structured output": "结构化输出",
|
||||
"Applies to": "适用范围",
|
||||
"Category": "类别",
|
||||
"Content": "内容",
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { FormEvent } from "react";
|
||||
import { Dialog } from "../shared/Dialog";
|
||||
import { Field } from "../shared/ui";
|
||||
import type { AppSnapshot, RiskLevel, TaskPriority } from "../../../shared/domain";
|
||||
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS } from "../../../shared/domain";
|
||||
import type { AppSnapshot, RiskLevel, TaskPriority, WritingMode } from "../../../shared/domain";
|
||||
import { RISK_LEVEL, TASK_PRIORITY, TASK_STATUS, WRITING_MODE } from "../../../shared/domain";
|
||||
import type { SnapshotActions } from "../app/useSnapshot";
|
||||
import { t } from "../i18n/i18n";
|
||||
|
||||
@@ -18,6 +18,12 @@ const PRIORITY_OPTIONS: ReadonlyArray<{ value: TaskPriority; label: string }> =
|
||||
{ value: TASK_PRIORITY.HIGH, label: t("High") },
|
||||
];
|
||||
|
||||
const WRITING_OPTIONS: ReadonlyArray<{ value: WritingMode; label: string }> = [
|
||||
{ value: WRITING_MODE.CONVERSATIONAL, label: t("Conversational (house style)") },
|
||||
{ value: WRITING_MODE.TECHNICAL, label: t("Technical spec") },
|
||||
{ value: WRITING_MODE.STRUCTURED, label: t("Structured output") },
|
||||
];
|
||||
|
||||
function value(form: FormData, key: string): string {
|
||||
return String(form.get(key) ?? "").trim();
|
||||
}
|
||||
@@ -59,6 +65,7 @@ export function NewTaskDialog({
|
||||
workspaceId: optional(form, "workspaceId"),
|
||||
priority: value(form, "priority") as TaskPriority,
|
||||
requiresFilesystem: form.get("requiresFilesystem") === "on",
|
||||
writingMode: value(form, "writingMode") as WritingMode,
|
||||
})).then((result) => {
|
||||
if (result) onClose();
|
||||
});
|
||||
@@ -102,6 +109,11 @@ export function NewTaskDialog({
|
||||
{PRIORITY_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label={t("Writing")}>
|
||||
<select className="input" name="writingMode" defaultValue={WRITING_MODE.CONVERSATIONAL}>
|
||||
{WRITING_OPTIONS.map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}
|
||||
</select>
|
||||
</Field>
|
||||
<label className="checkbox-row">
|
||||
<input type="checkbox" name="requiresFilesystem" /> {t("Requires workspace access")}
|
||||
</label>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApprovalState, IdentitySource, PrincipalType, RiskLevel, RoleKey, TaskPriority, TaskStatus, UiLanguage, UiTheme } from "../domain";
|
||||
import type { ApprovalState, IdentitySource, PrincipalType, RiskLevel, RoleKey, TaskPriority, TaskStatus, UiLanguage, UiTheme, WritingMode } from "../domain";
|
||||
|
||||
export interface SetActiveOrganizationInput {
|
||||
orgId: string;
|
||||
@@ -86,6 +86,8 @@ export interface CreateTaskInput {
|
||||
workspaceId: string | null;
|
||||
priority: TaskPriority;
|
||||
requiresFilesystem: boolean;
|
||||
/** Defaults to conversational. */
|
||||
writingMode?: WritingMode;
|
||||
/** Provenance: 'chat_wake' for mention-wake vehicles, 'goal_review' for CEO goal checkpoints, omitted for plain work. */
|
||||
origin?: "chat_wake" | "goal_review";
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ export interface ResolveAgentSkillsInput {
|
||||
taskText: string;
|
||||
/** The run's provider and model, matched against each skill's compatible_models. */
|
||||
runModel: { providerKey: string; model: string | null };
|
||||
/** False on technical and structured tasks, which skip the house-style skill. */
|
||||
houseStyle: boolean;
|
||||
}
|
||||
|
||||
export interface RunSkillTestsInput {
|
||||
|
||||
@@ -19,11 +19,14 @@ export const TASK_STATUS = { BACKLOG: "backlog", IN_PROGRESS: "in_progress", REV
|
||||
export const APPROVAL_STATE = { PENDING: "pending", APPROVED: "approved", DECLINED: "declined" } as const;
|
||||
export const RISK_LEVEL = { LOW: "low", MEDIUM: "medium", HIGH: "high" } as const;
|
||||
export const TASK_PRIORITY = { LOW: "low", NORMAL: "normal", HIGH: "high" } as const;
|
||||
/** How a task's output should read: conversational prose carries house style; technical and structured do not. */
|
||||
export const WRITING_MODE = { CONVERSATIONAL: "conversational", TECHNICAL: "technical", STRUCTURED: "structured" } as const;
|
||||
export const PROVIDER_AUTH_TYPE = { API_KEY: "api_key", OAUTH_PKCE: "oauth_pkce", ACCOUNT_SUBSCRIPTION: "account_subscription", CLOUD: "cloud" } as const;
|
||||
export const SOCIAL_PLATFORM = { X: "x", LINKEDIN: "linkedin", YOUTUBE: "youtube", INSTAGRAM: "instagram" } as const;
|
||||
export const FILE_SCAN_STATE = { APPROVED: "approved", BLOCKED: "blocked" } as const;
|
||||
|
||||
export type ValueOf<T> = T[keyof T];
|
||||
export type WritingMode = ValueOf<typeof WRITING_MODE>;
|
||||
export type TaskStatus = ValueOf<typeof TASK_STATUS>;
|
||||
export type ApprovalState = ValueOf<typeof APPROVAL_STATE>;
|
||||
export type RiskLevel = ValueOf<typeof RISK_LEVEL>;
|
||||
@@ -94,6 +97,7 @@ export interface TaskRecord {
|
||||
priority: TaskPriority;
|
||||
parentTaskId: string | null;
|
||||
requiresFilesystem: boolean;
|
||||
writingMode: WritingMode;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user