@xfey/tutti 0.1.93 → 0.1.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collaboration-state/clarification-records.d.ts +2 -1
- package/dist/collaboration-state/clarification-records.js +18 -0
- package/dist/collaboration-state/index.d.ts +3 -1
- package/dist/collaboration-state/index.js +2 -1
- package/dist/collaboration-state/messages.js +15 -0
- package/dist/collaboration-state/run-recording.js +18 -1
- package/dist/collaboration-state/run-results.js +6 -0
- package/dist/collaboration-state/task-retry.d.ts +16 -0
- package/dist/collaboration-state/task-retry.js +134 -0
- package/dist/control-plane/command-recovery.js +9 -1
- package/dist/control-plane/index.d.ts +3 -2
- package/dist/control-plane/index.js +14 -0
- package/dist/control-plane/run-retry.d.ts +19 -0
- package/dist/control-plane/run-retry.js +103 -0
- package/dist/control-plane/run-start.d.ts +2 -1
- package/dist/control-plane/run-start.js +18 -1
- package/dist/control-plane/task-compile-clarification-context.d.ts +4 -0
- package/dist/control-plane/task-compile-clarification-context.js +9 -22
- package/dist/control-plane/task-compile-continuation.js +2 -0
- package/dist/control-plane/task-compile-start.js +2 -0
- package/dist/control-plane/task-source-context-types.d.ts +11 -0
- package/dist/control-plane/task-source-context-types.js +2 -0
- package/dist/control-plane/task-source-context.d.ts +7 -0
- package/dist/control-plane/task-source-context.js +31 -0
- package/dist/control-plane/types.d.ts +15 -0
- package/dist/control-plane/workflows/openai.d.ts +1 -0
- package/dist/control-plane/workflows/openai.js +87 -15
- package/dist/control-plane/workflows/types.d.ts +6 -14
- package/dist/control-plane/worklist-terminal-feedback.js +3 -0
- package/dist/prompt-templates/index.d.ts +1 -1
- package/dist/prompt-templates/index.js +4 -3
- package/dist/providers/openai/app-server/durable-session-smoke-cli.d.ts +2 -0
- package/dist/providers/openai/app-server/durable-session-smoke-cli.js +229 -0
- package/dist/providers/openai/app-server/read-only-procedure.d.ts +10 -0
- package/dist/providers/openai/app-server/read-only-procedure.js +42 -13
- package/dist/providers/openai/app-server/session-store.d.ts +74 -0
- package/dist/providers/openai/app-server/session-store.js +305 -0
- package/dist/providers/openai/app-server/workspace-write-run.d.ts +17 -4
- package/dist/providers/openai/app-server/workspace-write-run.js +92 -28
- package/dist/providers/openai/codex-app-server.d.ts +1 -0
- package/dist/providers/openai/codex-app-server.js +2 -0
- package/dist/run-pipeline/openai.d.ts +1 -0
- package/dist/run-pipeline/openai.js +15 -43
- package/dist/run-pipeline/task-run-invocation.js +371 -93
- package/dist/server-shell/http/routes/project-api/execution-routes.js +32 -2
- package/dist/server-shell/http/routes/project-api/invalidations.d.ts +2 -1
- package/dist/server-shell/http/routes/project-api/invalidations.js +13 -0
- package/dist/server-shell/http/routes/project-api/openapi-execution-routes.d.ts +21 -0
- package/dist/server-shell/http/routes/project-api/openapi-execution-routes.js +12 -1
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +7 -0
- package/node_modules/@tutti/shared/dist/schemas/api/clarifications.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
- package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +5 -0
- package/node_modules/@tutti/shared/dist/schemas/api/messages.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +4 -1
- package/node_modules/@tutti/shared/dist/schemas/api/viewer-reference.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/work-items.d.ts +4 -0
- package/node_modules/@tutti/shared/dist/schemas/api/work-items.js +3 -0
- package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +33 -0
- package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +35 -1
- package/package.json +1 -1
- package/prompts/README.md +3 -1
- package/prompts/artifacts/README.md +1 -1
- package/prompts/artifacts/applicability.md +1 -1
- package/prompts/procedures/README.md +3 -3
- package/prompts/procedures/follow-up-check.md +1 -1
- package/prompts/procedures/reference-file-summary.md +1 -1
- package/prompts/procedures/task-compile.md +7 -7
- package/prompts/prompt-flow-map.md +16 -10
- package/prompts/runs/README.md +11 -16
- package/prompts/runs/task-execute.md +86 -0
- package/prompts/runs/task-finalize.md +11 -0
- package/prompts/runs/task-recovery.md +15 -0
- package/prompts/runs/task-result-contract.md +64 -0
- package/web/assets/{homepage-motion-scene-CLTkixIk.js → homepage-motion-scene-BFo4r4nz.js} +1 -1
- package/web/assets/index-CNIl4TF8.js +69 -0
- package/web/assets/index-quWLFJgD.css +1 -0
- package/web/index.html +2 -2
- package/prompts/runs/task-continuation.md +0 -199
- package/prompts/runs/task-retry.md +0 -207
- package/prompts/runs/task-run.md +0 -186
- package/web/assets/index-BTNYElB-.js +0 -69
- package/web/assets/index-Deu_eF5K.css +0 -1
|
@@ -6,6 +6,7 @@ import { projectBriefShouldRefreshForUpdatedPaths, refreshProjectBriefProjection
|
|
|
6
6
|
import { applyProjectDocsInitializationResult, buildProjectDocsInitializationContext, tryProjectContextBootstrapDocuments, } from "./project-context-bootstrap.js";
|
|
7
7
|
import { readActiveScratchpadRefreshActivityRef, readControlPlaneScratchpadSourceBatch, runControlPlaneScratchpadRefreshProcedure, } from "./scratchpad-refresh-start.js";
|
|
8
8
|
import { applyTaskCompileProcedureOutput } from "./task-compile-output.js";
|
|
9
|
+
import { materializePromptSourceMessages } from "./task-source-context.js";
|
|
9
10
|
import { materializeRecentReferences, readPendingRecentReferenceSummaryPaths, readRecentReferenceSources, } from "./recent-references.js";
|
|
10
11
|
import { startReferenceSummaryRefreshProcedure } from "./reference-summary-refresh.js";
|
|
11
12
|
export function startControlPlaneTaskCompile(options) {
|
|
@@ -431,6 +432,7 @@ export async function runControlPlaneTaskCompileProcedure(options) {
|
|
|
431
432
|
const output = await options.runner.runTaskCompile({
|
|
432
433
|
workflow_ref: options.workflowRef,
|
|
433
434
|
scratchpad: context.scratchpad_snapshot,
|
|
435
|
+
source_messages: materializePromptSourceMessages(options.store.db, context.sources),
|
|
434
436
|
worklist: readWorklistProjection(options.store.db),
|
|
435
437
|
...(recentReferences.length === 0
|
|
436
438
|
? {}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ClarificationRequestPayload } from "@tutti/shared/schemas/api";
|
|
2
|
+
export type PromptSourceMessage = {
|
|
3
|
+
role: "human" | "tutti";
|
|
4
|
+
author_display_name?: string;
|
|
5
|
+
text: string;
|
|
6
|
+
};
|
|
7
|
+
export type TaskCompileClarificationContext = {
|
|
8
|
+
request_payload: ClarificationRequestPayload;
|
|
9
|
+
answer_messages: PromptSourceMessage[];
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=task-source-context-types.d.ts.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SqliteDatabase } from "../store/index.js";
|
|
2
|
+
import type { PromptSourceMessage } from "./task-source-context-types.js";
|
|
3
|
+
export type TaskSourceMessageRef = {
|
|
4
|
+
message_id: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function materializePromptSourceMessages(db: SqliteDatabase, sources: TaskSourceMessageRef[]): PromptSourceMessage[];
|
|
7
|
+
//# sourceMappingURL=task-source-context.d.ts.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { readMessageProjectionById } from "../collaboration-state/index.js";
|
|
2
|
+
function promptSourceMessage(db, source) {
|
|
3
|
+
const message = readMessageProjectionById(db, source.message_id);
|
|
4
|
+
if (message === null) {
|
|
5
|
+
throw new Error("Task source message is unavailable");
|
|
6
|
+
}
|
|
7
|
+
if (message.scope.kind !== "main_chat") {
|
|
8
|
+
throw new Error("Task source message is outside main chat");
|
|
9
|
+
}
|
|
10
|
+
if (message.message_kind === "user_text" && message.author.kind === "human") {
|
|
11
|
+
const displayName = message.author.display_name?.trim();
|
|
12
|
+
return {
|
|
13
|
+
role: "human",
|
|
14
|
+
...(displayName === undefined || displayName === ""
|
|
15
|
+
? {}
|
|
16
|
+
: { author_display_name: displayName }),
|
|
17
|
+
text: message.body,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (message.message_kind === "agent_text" && message.author.kind === "agent") {
|
|
21
|
+
return {
|
|
22
|
+
role: "tutti",
|
|
23
|
+
text: message.body,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
throw new Error("Task source message has an unsupported kind or author");
|
|
27
|
+
}
|
|
28
|
+
export function materializePromptSourceMessages(db, sources) {
|
|
29
|
+
return sources.map((source) => promptSourceMessage(db, source));
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=task-source-context.js.map
|
|
@@ -5,6 +5,7 @@ import type { ActiveApprovalProjection, ClarificationRequestPayload, ExecutionRu
|
|
|
5
5
|
import type { HostProjectStore } from "../store/index.js";
|
|
6
6
|
import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
|
|
7
7
|
import type { RecentReferenceContext } from "./reference-context-types.js";
|
|
8
|
+
import type { PromptSourceMessage, TaskCompileClarificationContext } from "./task-source-context-types.js";
|
|
8
9
|
import type { ProcedureWorkflowRunnerResolver } from "./workflows/index.js";
|
|
9
10
|
export type Phase5ControlPlaneOptions = {
|
|
10
11
|
store: HostProjectStore;
|
|
@@ -84,6 +85,18 @@ export type RunContinuationContext = {
|
|
|
84
85
|
};
|
|
85
86
|
};
|
|
86
87
|
export type RunCorrectionContext = {
|
|
88
|
+
previous_provider_result: {
|
|
89
|
+
decision: "completed" | "completed_no_repo_changes";
|
|
90
|
+
summary: string;
|
|
91
|
+
user_note_candidate?: string;
|
|
92
|
+
} | {
|
|
93
|
+
decision: "needs_human";
|
|
94
|
+
summary: string;
|
|
95
|
+
clarification_request_payload: ClarificationRequestPayload;
|
|
96
|
+
} | {
|
|
97
|
+
decision: "failed";
|
|
98
|
+
summary: string;
|
|
99
|
+
};
|
|
87
100
|
summary: string;
|
|
88
101
|
reason_code: string;
|
|
89
102
|
guidance: string;
|
|
@@ -105,6 +118,8 @@ export type RunPipelineInput = {
|
|
|
105
118
|
dispatch_context: RunDispatchContext;
|
|
106
119
|
continuation_context?: RunContinuationContext;
|
|
107
120
|
correction_context?: RunCorrectionContext;
|
|
121
|
+
source_messages?: PromptSourceMessage[];
|
|
122
|
+
clarification_rounds?: TaskCompileClarificationContext[];
|
|
108
123
|
recent_references?: RecentReferenceContext[];
|
|
109
124
|
on_stage?: (progress: RunPipelineStageProgress) => void;
|
|
110
125
|
};
|
|
@@ -19,6 +19,7 @@ export type OpenAiProcedureWorkflowRunnerOptions = {
|
|
|
19
19
|
turnTimeoutMs?: number;
|
|
20
20
|
baseEnv?: NodeJS.ProcessEnv;
|
|
21
21
|
tuttiHome?: string;
|
|
22
|
+
projectId?: ProjectId;
|
|
22
23
|
userSkillsRoot?: string;
|
|
23
24
|
builtInSkillsRoot?: string;
|
|
24
25
|
agentContext?: CodexAppServerAgentContextRuntime;
|
|
@@ -7,7 +7,9 @@ import { createOpenAiStructuredOutputClient, runOpenAiStructuredOutput, } from "
|
|
|
7
7
|
import { readOpenAiApiKeyCredential } from "../../providers/openai/credential-store.js";
|
|
8
8
|
import { resolveOpenAiProviderConfig, } from "../../providers/openai/provider-config.js";
|
|
9
9
|
import { SCRATCHPAD_REFRESH_OUTPUT_SCHEMA, TASK_COMPILE_OUTPUT_SCHEMA, PROJECT_CONTEXT_BOOTSTRAP_OUTPUT_SCHEMA, CONTEXT_SYNC_OUTPUT_SCHEMA, PROJECT_BRIEF_REFRESH_OUTPUT_SCHEMA, FOLLOW_UP_CHECK_OUTPUT_SCHEMA, REFERENCE_FILE_SUMMARY_OUTPUT_SCHEMA, isContextSyncWorkflowOutput, isFollowUpCheckStructuredOutput, isProjectBriefRefreshWorkflowOutput, isProjectContextBootstrapWorkflowOutput, isReferenceFileSummaryStructuredOutput, isScratchpadRefreshStructuredOutput, isTaskCompileStructuredOutput, } from "./structured-output.js";
|
|
10
|
-
import { runCodexAppServerReadOnlyProcedure } from "../../providers/openai/app-server/read-only-procedure.js";
|
|
10
|
+
import { runCodexAppServerReadOnlyProcedure, } from "../../providers/openai/app-server/read-only-procedure.js";
|
|
11
|
+
import { CODEX_APP_SERVER_VERSION } from "../../providers/openai/codex-app-server.js";
|
|
12
|
+
import { acquireCodexTaskSessionLock, createProviderFingerprint, markCodexTaskSessionArchived, openCodexDurableSession, recordCodexSessionArchiveFailure, recordCodexTaskSessionThread, } from "../../providers/openai/app-server/session-store.js";
|
|
11
13
|
import { getProjectUserSkillsRoot } from "../../skills/index.js";
|
|
12
14
|
function readOnlyAppServerOptions(options, prompt, outputSchema, skillSelectionName) {
|
|
13
15
|
if (options.readOnlyAppServer === undefined) {
|
|
@@ -70,6 +72,56 @@ function readOnlyRuntimeTelemetry(context, stageId) {
|
|
|
70
72
|
},
|
|
71
73
|
};
|
|
72
74
|
}
|
|
75
|
+
async function runDurableReadOnlyProcedure(input) {
|
|
76
|
+
const runtime = input.options.readOnlyAppServer;
|
|
77
|
+
if (runtime?.tuttiHome === undefined ||
|
|
78
|
+
runtime.projectId === undefined ||
|
|
79
|
+
input.context === undefined) {
|
|
80
|
+
return runCodexAppServerReadOnlyProcedure(input.invocation);
|
|
81
|
+
}
|
|
82
|
+
let session = openCodexDurableSession({
|
|
83
|
+
tuttiHome: runtime.tuttiHome,
|
|
84
|
+
projectId: runtime.projectId,
|
|
85
|
+
kind: input.kind,
|
|
86
|
+
ownerRef: input.context.workflow_ref,
|
|
87
|
+
compatibility: {
|
|
88
|
+
codexVersion: CODEX_APP_SERVER_VERSION,
|
|
89
|
+
model: input.options.config.default_model,
|
|
90
|
+
providerFingerprint: createProviderFingerprint({
|
|
91
|
+
providerKind: "openai",
|
|
92
|
+
...(input.options.config.api_base_url === undefined
|
|
93
|
+
? {}
|
|
94
|
+
: { apiBaseUrl: input.options.config.api_base_url }),
|
|
95
|
+
}),
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const release = acquireCodexTaskSessionLock(session);
|
|
99
|
+
try {
|
|
100
|
+
return await runCodexAppServerReadOnlyProcedure({
|
|
101
|
+
...input.invocation,
|
|
102
|
+
session: {
|
|
103
|
+
codexHome: session.codexHome,
|
|
104
|
+
...(session.metadata.thread_id === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: { threadId: session.metadata.thread_id }),
|
|
107
|
+
...(session.metadata.lifecycle === "archived" ? { unarchive: true } : {}),
|
|
108
|
+
onThreadReady: (threadId) => {
|
|
109
|
+
session = recordCodexTaskSessionThread(session, threadId);
|
|
110
|
+
},
|
|
111
|
+
archiveAfterTurn: input.archiveAfterTurn,
|
|
112
|
+
onThreadArchived: () => {
|
|
113
|
+
session = markCodexTaskSessionArchived(session);
|
|
114
|
+
},
|
|
115
|
+
onThreadArchiveFailure: () => {
|
|
116
|
+
recordCodexSessionArchiveFailure(session);
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
release();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
73
125
|
function referenceSummarySourcePath(options, input) {
|
|
74
126
|
if (options.readOnlyAppServer === undefined) {
|
|
75
127
|
throw new Error("Codex app-server read-only Procedure adapter is not configured");
|
|
@@ -153,6 +205,7 @@ export function buildTaskCompilePromptInput(input) {
|
|
|
153
205
|
open_questions: input.scratchpad.open_questions,
|
|
154
206
|
task_changes: input.scratchpad.task_changes,
|
|
155
207
|
},
|
|
208
|
+
source_messages: input.source_messages.map((message) => ({ ...message })),
|
|
156
209
|
worklist: {
|
|
157
210
|
modules: input.worklist.modules.map((entry) => ({
|
|
158
211
|
id: entry.module.id,
|
|
@@ -283,12 +336,18 @@ export function createOpenAiProcedureWorkflowRunner(options) {
|
|
|
283
336
|
workflow_input_json: buildTaskCompilePromptInput(input),
|
|
284
337
|
},
|
|
285
338
|
});
|
|
286
|
-
const result = await
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
339
|
+
const result = await runDurableReadOnlyProcedure({
|
|
340
|
+
options,
|
|
341
|
+
context,
|
|
342
|
+
kind: "task_compile",
|
|
343
|
+
archiveAfterTurn: (output) => "proposal" in output.result,
|
|
344
|
+
invocation: {
|
|
345
|
+
...readOnlyAppServerOptions(options, instructions, TASK_COMPILE_OUTPUT_SCHEMA, "task_compile"),
|
|
346
|
+
...readOnlyRuntimeTelemetry(context, "task_compile"),
|
|
347
|
+
validateOutput: isTaskCompileStructuredOutput,
|
|
348
|
+
networkAccess: false,
|
|
349
|
+
webSearchMode: "disabled",
|
|
350
|
+
},
|
|
292
351
|
});
|
|
293
352
|
recordUsage(options, "task_compile", result, {
|
|
294
353
|
workflow_ref: context?.workflow_ref ?? input.workflow_ref,
|
|
@@ -332,10 +391,16 @@ export function createOpenAiProcedureWorkflowRunner(options) {
|
|
|
332
391
|
workflow_input_json: input,
|
|
333
392
|
},
|
|
334
393
|
});
|
|
335
|
-
const result = await
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
394
|
+
const result = await runDurableReadOnlyProcedure({
|
|
395
|
+
options,
|
|
396
|
+
context,
|
|
397
|
+
kind: "project_context_bootstrap",
|
|
398
|
+
archiveAfterTurn: () => true,
|
|
399
|
+
invocation: {
|
|
400
|
+
...readOnlyAppServerOptions(options, instructions, PROJECT_CONTEXT_BOOTSTRAP_OUTPUT_SCHEMA, "project_context_bootstrap"),
|
|
401
|
+
...readOnlyRuntimeTelemetry(context, "project_context_bootstrap"),
|
|
402
|
+
validateOutput: isProjectContextBootstrapWorkflowOutput,
|
|
403
|
+
},
|
|
339
404
|
});
|
|
340
405
|
recordUsage(options, "project_context_bootstrap", result, {
|
|
341
406
|
...(context?.workflow_ref === undefined ? {} : { workflow_ref: context.workflow_ref }),
|
|
@@ -348,10 +413,16 @@ export function createOpenAiProcedureWorkflowRunner(options) {
|
|
|
348
413
|
templateKey: "procedures.context_sync",
|
|
349
414
|
...(options.promptsRoot === undefined ? {} : { promptsRoot: options.promptsRoot }),
|
|
350
415
|
});
|
|
351
|
-
const result = await
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
416
|
+
const result = await runDurableReadOnlyProcedure({
|
|
417
|
+
options,
|
|
418
|
+
context,
|
|
419
|
+
kind: "context_sync",
|
|
420
|
+
archiveAfterTurn: () => true,
|
|
421
|
+
invocation: {
|
|
422
|
+
...readOnlyAppServerOptions(options, instructions, CONTEXT_SYNC_OUTPUT_SCHEMA, "context_sync"),
|
|
423
|
+
...readOnlyRuntimeTelemetry(context, "context_sync"),
|
|
424
|
+
validateOutput: isContextSyncWorkflowOutput,
|
|
425
|
+
},
|
|
355
426
|
});
|
|
356
427
|
recordUsage(options, "context_sync", result, {
|
|
357
428
|
workflow_ref: context?.workflow_ref ?? input.workflow_ref,
|
|
@@ -425,6 +496,7 @@ export function resolveProjectOpenAiProcedureWorkflowRunner(options) {
|
|
|
425
496
|
apiKey: credential.secret,
|
|
426
497
|
workspaceRoot: options.workspaceRoot,
|
|
427
498
|
tuttiHome: options.tuttiHome,
|
|
499
|
+
projectId: options.projectId,
|
|
428
500
|
userSkillsRoot: getProjectUserSkillsRoot(options.tuttiHome, options.projectId),
|
|
429
501
|
...(options.agentContext === undefined ? {} : { agentContext: options.agentContext }),
|
|
430
502
|
...(options.invocationCoordinator === undefined
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExecutionRuntimeSnapshot, MessageProjection, RunResultProjection, TaskDetailProjection, WorklistProjection } from "@tutti/shared/schemas/api";
|
|
2
2
|
import type { RecentReferenceContext } from "../reference-context-types.js";
|
|
3
|
+
import type { PromptSourceMessage, TaskCompileClarificationContext } from "../task-source-context-types.js";
|
|
3
4
|
import type { ClarificationRequestPayload, TaskContractProjection } from "@tutti/shared/schemas/api";
|
|
4
5
|
import type { ProjectDocKey } from "@tutti/shared/schemas/api";
|
|
5
6
|
import type { ActivityRef, ClarificationRoundRef, ClarificationThreadRef, MessageId, TaskId, TaskModuleId, WorkflowInvocationRef } from "@tutti/shared/ids";
|
|
@@ -28,11 +29,7 @@ export type ScratchpadRefreshWorklistInput = {
|
|
|
28
29
|
}>;
|
|
29
30
|
}>;
|
|
30
31
|
};
|
|
31
|
-
export type ScratchpadRefreshSourceMessageInput =
|
|
32
|
-
role: "human" | "tutti";
|
|
33
|
-
author_display_name?: string;
|
|
34
|
-
text: string;
|
|
35
|
-
};
|
|
32
|
+
export type ScratchpadRefreshSourceMessageInput = PromptSourceMessage;
|
|
36
33
|
export type ScratchpadRefreshWorkflowContext = {
|
|
37
34
|
workflow_ref: WorkflowInvocationRef;
|
|
38
35
|
activity_ref: ActivityRef;
|
|
@@ -80,12 +77,10 @@ export type TaskCompileProposalTask = {
|
|
|
80
77
|
export type TaskCompileWorkflowInput = {
|
|
81
78
|
workflow_ref: WorkflowInvocationRef;
|
|
82
79
|
scratchpad: ScratchpadWorkflowSnapshot;
|
|
80
|
+
source_messages: PromptSourceMessage[];
|
|
83
81
|
worklist: WorklistProjection;
|
|
84
82
|
recent_references?: RecentReferenceContext[];
|
|
85
|
-
clarification_rounds?:
|
|
86
|
-
request_payload: ClarificationRequestPayload;
|
|
87
|
-
answer_messages: TaskCompilePromptClarificationMessage[];
|
|
88
|
-
}>;
|
|
83
|
+
clarification_rounds?: TaskCompileClarificationContext[];
|
|
89
84
|
};
|
|
90
85
|
export type TaskCompileWorkflowOutput = {
|
|
91
86
|
decision: "ready";
|
|
@@ -102,6 +97,7 @@ export type TaskCompilePromptInput = {
|
|
|
102
97
|
open_questions: string[];
|
|
103
98
|
task_changes: string[];
|
|
104
99
|
};
|
|
100
|
+
source_messages: PromptSourceMessage[];
|
|
105
101
|
worklist: {
|
|
106
102
|
modules: Array<{
|
|
107
103
|
id: TaskModuleId;
|
|
@@ -125,11 +121,7 @@ export type TaskCompilePromptInput = {
|
|
|
125
121
|
answer_messages: TaskCompilePromptClarificationMessage[];
|
|
126
122
|
}>;
|
|
127
123
|
};
|
|
128
|
-
export type TaskCompilePromptClarificationMessage =
|
|
129
|
-
role: "human" | "tutti";
|
|
130
|
-
author_display_name?: string;
|
|
131
|
-
text: string;
|
|
132
|
-
};
|
|
124
|
+
export type TaskCompilePromptClarificationMessage = PromptSourceMessage;
|
|
133
125
|
export type TaskCompileStructuredOutput = {
|
|
134
126
|
result: {
|
|
135
127
|
proposal: TaskCompileStructuredProposal;
|
|
@@ -272,6 +272,9 @@ export function emitTaskFailedResultMessage(input) {
|
|
|
272
272
|
: { latest_run_result_id: item.latest_run_result_id }),
|
|
273
273
|
...(input.activityRef === undefined ? {} : { activity_ref: input.activityRef }),
|
|
274
274
|
...(errorCode === undefined ? {} : { error_code: errorCode }),
|
|
275
|
+
...(entry.detail?.detail.result.retry_action === undefined
|
|
276
|
+
? {}
|
|
277
|
+
: { retry_action: entry.detail.detail.result.retry_action }),
|
|
275
278
|
};
|
|
276
279
|
return publishTaskResultMessage(input, result);
|
|
277
280
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type PromptTemplateKey = "procedures.project_context_bootstrap" | "procedures.context_sync" | "procedures.project_brief_refresh" | "procedures.scratchpad_refresh" | "procedures.task_compile" | "procedures.reference_file_summary" | "procedures.follow_up_check" | "runs.
|
|
1
|
+
export type PromptTemplateKey = "procedures.project_context_bootstrap" | "procedures.context_sync" | "procedures.project_brief_refresh" | "procedures.scratchpad_refresh" | "procedures.task_compile" | "procedures.reference_file_summary" | "procedures.follow_up_check" | "runs.task_execute" | "runs.task_finalize" | "runs.task_recovery" | "runs.task_result_contract" | "artifacts.applicability" | "chat.assistant" | "codex.no_write_smoke";
|
|
2
2
|
export type RenderPromptTemplateOptions = {
|
|
3
3
|
templateKey: PromptTemplateKey;
|
|
4
4
|
variables?: Record<string, unknown>;
|
|
@@ -19,9 +19,10 @@ const TEMPLATE_REGISTRY = {
|
|
|
19
19
|
"procedures.task_compile": "procedures/task-compile.md",
|
|
20
20
|
"procedures.reference_file_summary": "procedures/reference-file-summary.md",
|
|
21
21
|
"procedures.follow_up_check": "procedures/follow-up-check.md",
|
|
22
|
-
"runs.
|
|
23
|
-
"runs.
|
|
24
|
-
"runs.
|
|
22
|
+
"runs.task_execute": "runs/task-execute.md",
|
|
23
|
+
"runs.task_finalize": "runs/task-finalize.md",
|
|
24
|
+
"runs.task_recovery": "runs/task-recovery.md",
|
|
25
|
+
"runs.task_result_contract": "runs/task-result-contract.md",
|
|
25
26
|
"artifacts.applicability": "artifacts/applicability.md",
|
|
26
27
|
"chat.assistant": "chat-assistant.md",
|
|
27
28
|
"codex.no_write_smoke": "codex/no-write-smoke.md",
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, } from "node:fs";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import { join, relative } from "node:path";
|
|
6
|
+
import { createProjectId, createTaskId } from "@tutti/shared/ids";
|
|
7
|
+
import { CODEX_APP_SERVER_VERSION } from "../codex-app-server.js";
|
|
8
|
+
import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
|
|
9
|
+
import { runCodexAppServerReadOnlyProcedure } from "./read-only-procedure.js";
|
|
10
|
+
import { resolveCodexAppServerSmokeProviderInput } from "./smoke-provider-input.js";
|
|
11
|
+
import { acquireCodexTaskSessionLock, createProviderFingerprint, markCodexTaskSessionArchived, openCodexTaskSession, recordCodexTaskSessionThread, } from "./session-store.js";
|
|
12
|
+
import { runCodexAppServerWorkspaceWriteRun } from "./workspace-write-run.js";
|
|
13
|
+
const WRITE_SENTINEL = "TUTTI_DURABLE_WRITE";
|
|
14
|
+
const RESULT_SCHEMA = {
|
|
15
|
+
type: "object",
|
|
16
|
+
additionalProperties: false,
|
|
17
|
+
properties: {
|
|
18
|
+
sentinel: { type: "string", enum: [WRITE_SENTINEL] },
|
|
19
|
+
file_present: { type: "boolean" },
|
|
20
|
+
},
|
|
21
|
+
required: ["sentinel", "file_present"],
|
|
22
|
+
};
|
|
23
|
+
function isResult(value) {
|
|
24
|
+
return (typeof value === "object" &&
|
|
25
|
+
value !== null &&
|
|
26
|
+
!Array.isArray(value) &&
|
|
27
|
+
value.sentinel === WRITE_SENTINEL &&
|
|
28
|
+
value.file_present === true);
|
|
29
|
+
}
|
|
30
|
+
function mode(path) {
|
|
31
|
+
return statSync(path).mode & 0o777;
|
|
32
|
+
}
|
|
33
|
+
function requireMode(path, expected) {
|
|
34
|
+
const actual = mode(path);
|
|
35
|
+
if (actual !== expected) {
|
|
36
|
+
throw new Error(`Durable session path has mode ${actual.toString(8)}; expected ${expected.toString(8)}.`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function fingerprintPath(path) {
|
|
40
|
+
if (!existsSync(path)) {
|
|
41
|
+
return "missing";
|
|
42
|
+
}
|
|
43
|
+
const hash = createHash("sha256");
|
|
44
|
+
const visit = (current) => {
|
|
45
|
+
const entry = lstatSync(current);
|
|
46
|
+
hash.update(JSON.stringify({
|
|
47
|
+
path: relative(path, current),
|
|
48
|
+
mode: entry.mode & 0o777,
|
|
49
|
+
size: entry.size,
|
|
50
|
+
modified_ms: entry.mtimeMs,
|
|
51
|
+
type: entry.isDirectory() ? "directory" : entry.isSymbolicLink() ? "symlink" : "file",
|
|
52
|
+
}));
|
|
53
|
+
if (!entry.isDirectory()) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
for (const child of readdirSync(current).sort()) {
|
|
57
|
+
visit(join(current, child));
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
visit(path);
|
|
61
|
+
return hash.digest("hex");
|
|
62
|
+
}
|
|
63
|
+
function userCodexConfigurationFingerprint() {
|
|
64
|
+
const root = join(homedir(), ".codex");
|
|
65
|
+
const hash = createHash("sha256");
|
|
66
|
+
for (const relativePath of ["auth.json", "config.toml", "plugins", "skills"]) {
|
|
67
|
+
hash.update(relativePath);
|
|
68
|
+
hash.update(fingerprintPath(join(root, relativePath)));
|
|
69
|
+
}
|
|
70
|
+
return hash.digest("hex");
|
|
71
|
+
}
|
|
72
|
+
async function withSessionLock(session, run) {
|
|
73
|
+
const release = acquireCodexTaskSessionLock(session);
|
|
74
|
+
try {
|
|
75
|
+
return await run();
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
release();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function main() {
|
|
82
|
+
const provider = resolveCodexAppServerSmokeProviderInput();
|
|
83
|
+
const model = provider.defaultModel ?? DEFAULT_OPENAI_MODEL;
|
|
84
|
+
const smokeRoot = mkdtempSync(join(tmpdir(), "tutti-codex-durable-session-smoke-"));
|
|
85
|
+
const tuttiHome = join(smokeRoot, "tutti-home");
|
|
86
|
+
const workspaceRoot = join(smokeRoot, "repo");
|
|
87
|
+
const projectId = createProjectId();
|
|
88
|
+
const taskId = createTaskId();
|
|
89
|
+
const userCodexBefore = userCodexConfigurationFingerprint();
|
|
90
|
+
let session = openCodexTaskSession({
|
|
91
|
+
tuttiHome,
|
|
92
|
+
projectId,
|
|
93
|
+
taskId,
|
|
94
|
+
compatibility: {
|
|
95
|
+
codexVersion: CODEX_APP_SERVER_VERSION,
|
|
96
|
+
model,
|
|
97
|
+
providerFingerprint: createProviderFingerprint({
|
|
98
|
+
providerKind: "openai",
|
|
99
|
+
...(provider.apiBaseUrl === undefined ? {} : { apiBaseUrl: provider.apiBaseUrl }),
|
|
100
|
+
}),
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
execFileSync("git", ["init", workspaceRoot], { stdio: "ignore" });
|
|
105
|
+
execFileSync("git", [
|
|
106
|
+
"-C",
|
|
107
|
+
workspaceRoot,
|
|
108
|
+
"config",
|
|
109
|
+
"user.email",
|
|
110
|
+
"tutti-smoke@example.invalid",
|
|
111
|
+
]);
|
|
112
|
+
execFileSync("git", ["-C", workspaceRoot, "config", "user.name", "Tutti Smoke"]);
|
|
113
|
+
const common = {
|
|
114
|
+
apiKey: provider.apiKey,
|
|
115
|
+
...(provider.apiBaseUrl === undefined ? {} : { apiBaseUrl: provider.apiBaseUrl }),
|
|
116
|
+
defaultModel: model,
|
|
117
|
+
workspaceRoot,
|
|
118
|
+
networkAccess: false,
|
|
119
|
+
strictLeakCheck: true,
|
|
120
|
+
};
|
|
121
|
+
const execute = await withSessionLock(session, async () => await runCodexAppServerWorkspaceWriteRun({
|
|
122
|
+
...common,
|
|
123
|
+
approvalPolicy: "never",
|
|
124
|
+
prompt: `Create durable-smoke.txt in the workspace root with exactly one line: ${WRITE_SENTINEL}. ` +
|
|
125
|
+
"Do not modify any other file. Then briefly report completion.",
|
|
126
|
+
session: {
|
|
127
|
+
codexHome: session.codexHome,
|
|
128
|
+
onThreadReady: (threadId) => {
|
|
129
|
+
session = recordCodexTaskSessionThread(session, threadId);
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
if (readFileSync(join(workspaceRoot, "durable-smoke.txt"), "utf8") !== `${WRITE_SENTINEL}\n`) {
|
|
134
|
+
throw new Error("Workspace-write turn did not create the exact durable smoke fixture.");
|
|
135
|
+
}
|
|
136
|
+
const threadId = session.metadata.thread_id;
|
|
137
|
+
if (threadId === undefined) {
|
|
138
|
+
throw new Error("Workspace-write turn did not persist a durable thread id.");
|
|
139
|
+
}
|
|
140
|
+
const finalize = await withSessionLock(session, async () => await runCodexAppServerReadOnlyProcedure({
|
|
141
|
+
...common,
|
|
142
|
+
prompt: "Read durable-smoke.txt and return the supplied JSON shape. " +
|
|
143
|
+
`Use sentinel ${WRITE_SENTINEL} and set file_present to true.`,
|
|
144
|
+
outputSchema: RESULT_SCHEMA,
|
|
145
|
+
validateOutput: isResult,
|
|
146
|
+
session: {
|
|
147
|
+
codexHome: session.codexHome,
|
|
148
|
+
threadId,
|
|
149
|
+
archiveAfterTurn: () => true,
|
|
150
|
+
onThreadArchived: () => {
|
|
151
|
+
session = markCodexTaskSessionArchived(session);
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}));
|
|
155
|
+
if (session.metadata.lifecycle !== "archived") {
|
|
156
|
+
throw new Error("Durable smoke thread was not archived after the read-only turn.");
|
|
157
|
+
}
|
|
158
|
+
const recovery = await withSessionLock(session, async () => await runCodexAppServerReadOnlyProcedure({
|
|
159
|
+
...common,
|
|
160
|
+
prompt: "After resuming this archived thread, read durable-smoke.txt and return the supplied " +
|
|
161
|
+
`JSON shape with sentinel ${WRITE_SENTINEL} and file_present true.`,
|
|
162
|
+
outputSchema: RESULT_SCHEMA,
|
|
163
|
+
validateOutput: isResult,
|
|
164
|
+
session: {
|
|
165
|
+
codexHome: session.codexHome,
|
|
166
|
+
threadId,
|
|
167
|
+
unarchive: true,
|
|
168
|
+
onThreadReady: (threadId) => {
|
|
169
|
+
session = recordCodexTaskSessionThread(session, threadId);
|
|
170
|
+
},
|
|
171
|
+
archiveAfterTurn: () => true,
|
|
172
|
+
onThreadArchived: () => {
|
|
173
|
+
session = markCodexTaskSessionArchived(session);
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
}));
|
|
177
|
+
const authPath = join(session.codexHome, "auth.json");
|
|
178
|
+
const sessionIndexPath = join(tuttiHome, "projects", projectId, "codex-sessions", "task-index", `${taskId}.json`);
|
|
179
|
+
requireMode(session.root, 0o700);
|
|
180
|
+
requireMode(session.codexHome, 0o700);
|
|
181
|
+
requireMode(session.metadataPath, 0o600);
|
|
182
|
+
requireMode(sessionIndexPath, 0o600);
|
|
183
|
+
requireMode(authPath, 0o600);
|
|
184
|
+
const codexHomeEntries = readdirSync(session.codexHome);
|
|
185
|
+
const repoSessionPaths = readdirSync(workspaceRoot).filter((entry) => entry === ".codex" || entry === "codex-sessions" || entry === "auth.json");
|
|
186
|
+
if (repoSessionPaths.length > 0) {
|
|
187
|
+
throw new Error("Durable session state was written into the target repository.");
|
|
188
|
+
}
|
|
189
|
+
if (userCodexConfigurationFingerprint() !== userCodexBefore) {
|
|
190
|
+
throw new Error("The durable smoke changed user-level Codex configuration.");
|
|
191
|
+
}
|
|
192
|
+
if (execute.thread_id !== finalize.thread_id || finalize.thread_id !== recovery.thread_id) {
|
|
193
|
+
throw new Error("Durable smoke turns did not resume the same app-server thread.");
|
|
194
|
+
}
|
|
195
|
+
console.log(JSON.stringify({
|
|
196
|
+
provider_source: provider.source,
|
|
197
|
+
status: "passed",
|
|
198
|
+
codex_version: CODEX_APP_SERVER_VERSION,
|
|
199
|
+
model,
|
|
200
|
+
same_thread_across_processes: true,
|
|
201
|
+
per_turn_output_schema: true,
|
|
202
|
+
workspace_write_then_read_only: true,
|
|
203
|
+
archive_then_unarchive: true,
|
|
204
|
+
final_lifecycle: session.metadata.lifecycle,
|
|
205
|
+
private_session_root_mode: mode(session.root).toString(8),
|
|
206
|
+
private_codex_home_mode: mode(session.codexHome).toString(8),
|
|
207
|
+
private_metadata_mode: mode(session.metadataPath).toString(8),
|
|
208
|
+
private_auth_json_mode: mode(authPath).toString(8),
|
|
209
|
+
auth_json_in_private_home: true,
|
|
210
|
+
plugin_cache_in_private_home: existsSync(join(session.codexHome, ".tmp", "plugins")),
|
|
211
|
+
state_store_in_private_home: codexHomeEntries.some((entry) => /^state_\d+\.sqlite$/u.test(entry)),
|
|
212
|
+
shell_snapshots_in_private_home: existsSync(join(session.codexHome, "shell_snapshots")),
|
|
213
|
+
user_codex_configuration_unchanged: true,
|
|
214
|
+
repo_session_files: 0,
|
|
215
|
+
}, null, 2));
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
rmSync(smokeRoot, { recursive: true, force: true });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
main().catch((error) => {
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
console.error(JSON.stringify({
|
|
224
|
+
status: "failed",
|
|
225
|
+
message,
|
|
226
|
+
}, null, 2));
|
|
227
|
+
process.exitCode = 1;
|
|
228
|
+
});
|
|
229
|
+
//# sourceMappingURL=durable-session-smoke-cli.js.map
|
|
@@ -14,6 +14,15 @@ export type CodexAppServerReadOnlyProcedureInput<TOutput> = {
|
|
|
14
14
|
prompt: string;
|
|
15
15
|
outputSchema: unknown;
|
|
16
16
|
validateOutput: (value: unknown) => value is TOutput;
|
|
17
|
+
session?: {
|
|
18
|
+
codexHome: string;
|
|
19
|
+
threadId?: string;
|
|
20
|
+
unarchive?: boolean;
|
|
21
|
+
onThreadReady?: (threadId: string) => void | Promise<void>;
|
|
22
|
+
archiveAfterTurn?: (output: TOutput) => boolean;
|
|
23
|
+
onThreadArchived?: () => void | Promise<void>;
|
|
24
|
+
onThreadArchiveFailure?: () => void | Promise<void>;
|
|
25
|
+
};
|
|
17
26
|
networkAccess?: boolean;
|
|
18
27
|
webSearchMode?: CodexAppServerWebSearchMode;
|
|
19
28
|
skillSelectionName?: CodexAppServerSkillSelectionName;
|
|
@@ -34,6 +43,7 @@ export type CodexAppServerReadOnlyProcedureInput<TOutput> = {
|
|
|
34
43
|
};
|
|
35
44
|
export type CodexAppServerReadOnlyProcedureResult<TOutput> = {
|
|
36
45
|
output: TOutput;
|
|
46
|
+
thread_id: string;
|
|
37
47
|
model: string;
|
|
38
48
|
turn_status: "completed";
|
|
39
49
|
notification_count: number;
|