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:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-10-08 07:28:47 -04:00
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'));
`;
+2
View File
@@ -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;
+1 -1
View File
@@ -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)
+9 -2
View File
@@ -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);
+4 -3
View File
@@ -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,
+3 -3
View File
@@ -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,
+5 -5
View File
@@ -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
+15 -1
View File
@@ -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}`);
+4
View File
@@ -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": "内容",
+14 -2
View File
@@ -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>
+3 -1
View File
@@ -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";
}
+2
View File
@@ -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 {
+4
View File
@@ -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;
}