@xfey/tutti 0.1.10 → 0.1.12

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.
Files changed (57) hide show
  1. package/dist/control-plane/context-sync.js +1 -0
  2. package/dist/control-plane/event-publishers.js +21 -12
  3. package/dist/control-plane/execution-status.js +4 -0
  4. package/dist/control-plane/project-context-bootstrap.d.ts +2 -0
  5. package/dist/control-plane/project-context-bootstrap.js +1 -0
  6. package/dist/control-plane/reference-summary-refresh.js +4 -0
  7. package/dist/control-plane/run-start.js +1 -0
  8. package/dist/control-plane/task-compile-continuation.js +1 -0
  9. package/dist/control-plane/task-compile-start.d.ts +2 -1
  10. package/dist/control-plane/task-compile-start.js +5 -0
  11. package/dist/control-plane/types.d.ts +2 -1
  12. package/dist/control-plane/workflows/openai.js +19 -0
  13. package/dist/control-plane/workflows/types.d.ts +6 -1
  14. package/dist/procedure-engine/index.d.ts +10 -1
  15. package/dist/procedure-engine/index.js +34 -4
  16. package/dist/providers/openai/app-server/json-rpc.d.ts +6 -1
  17. package/dist/providers/openai/app-server/json-rpc.js +15 -1
  18. package/dist/providers/openai/app-server/read-only-procedure.d.ts +2 -0
  19. package/dist/providers/openai/app-server/read-only-procedure.js +13 -1
  20. package/dist/providers/openai/app-server/runtime-telemetry.d.ts +18 -0
  21. package/dist/providers/openai/app-server/runtime-telemetry.js +130 -0
  22. package/dist/providers/openai/app-server/workspace-write-run.d.ts +2 -0
  23. package/dist/providers/openai/app-server/workspace-write-run.js +13 -1
  24. package/dist/run-engine/index.d.ts +2 -1
  25. package/dist/run-engine/index.js +14 -1
  26. package/dist/run-pipeline/task-run-invocation.js +9 -0
  27. package/dist/server-shell/cli/args.d.ts +9 -9
  28. package/dist/server-shell/cli/args.js +31 -31
  29. package/dist/server-shell/cli/cli.js +27 -27
  30. package/dist/server-shell/cli/errors.d.ts +2 -1
  31. package/dist/server-shell/cli/errors.js +55 -12
  32. package/dist/server-shell/cli/host-lifecycle.js +7 -0
  33. package/dist/server-shell/cli/launch-command.d.ts +2 -2
  34. package/dist/server-shell/cli/launch-command.js +25 -11
  35. package/dist/server-shell/cli/launch.js +0 -3
  36. package/dist/server-shell/cli/managed-host.d.ts +2 -0
  37. package/dist/server-shell/cli/managed-host.js +9 -18
  38. package/dist/server-shell/cli/project-resolver.d.ts +6 -0
  39. package/dist/server-shell/cli/project-resolver.js +125 -3
  40. package/dist/server-shell/cli/provider-tui.d.ts +19 -0
  41. package/dist/server-shell/cli/provider-tui.js +64 -38
  42. package/dist/server-shell/cli/runtime-commands.d.ts +5 -5
  43. package/dist/server-shell/cli/runtime-commands.js +68 -25
  44. package/dist/server-shell/cli/terminal-qr.d.ts +5 -0
  45. package/dist/server-shell/cli/terminal-qr.js +17 -0
  46. package/dist/server-shell/http/routes/project-api/openapi-execution-routes.d.ts +14 -0
  47. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +14 -0
  48. package/dist/server-shell/http/routes/project-api/openapi.d.ts +28 -0
  49. package/dist/server-shell/http/routes/project-api/schemas.d.ts +14 -0
  50. package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.d.ts +50 -0
  51. package/node_modules/@tutti/shared/dist/schemas/api/runtime-events.js +25 -0
  52. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +4 -1
  53. package/package.json +1 -1
  54. package/web/assets/index-CDUT1Etl.js +29 -0
  55. package/web/assets/{index-D8xJY8Gr.css → index-DIQpGeKY.css} +1 -1
  56. package/web/index.html +2 -2
  57. package/web/assets/index-BMMQgQjz.js +0 -29
@@ -70,6 +70,7 @@ export function startContextSyncAnalysisProcedure(input) {
70
70
  const output = await runnerResolution.runner.runContextSync?.(workflowInput, {
71
71
  workflow_ref: workflowRef,
72
72
  activity_ref: context.activity_ref,
73
+ reportProgress: context.reportProgress,
73
74
  });
74
75
  if (output === undefined) {
75
76
  return {
@@ -135,18 +135,27 @@ export function publishRunTransition(input) {
135
135
  }
136
136
  }
137
137
  export function publishProcedureTransition(input) {
138
- const eventPayload = input.state.activity.status === "running"
139
- ? undefined
140
- : {
141
- terminal: {
142
- result_kind: input.state.activity.status === "finished"
143
- ? "finished"
144
- : input.state.activity.status === "needs_human"
145
- ? "needs_human"
146
- : "failed",
147
- summary: input.state.activity.summary,
138
+ const eventPayload = input.state.transition_kind === "progress"
139
+ ? {
140
+ progress: {
141
+ message: input.state.progress?.summary ?? input.state.activity.summary,
142
+ ...(input.state.progress?.phase === undefined
143
+ ? {}
144
+ : { phase: input.state.progress.phase }),
148
145
  },
149
- };
146
+ }
147
+ : input.state.activity.status === "running"
148
+ ? undefined
149
+ : {
150
+ terminal: {
151
+ result_kind: input.state.activity.status === "finished"
152
+ ? "finished"
153
+ : input.state.activity.status === "needs_human"
154
+ ? "needs_human"
155
+ : "failed",
156
+ summary: input.state.activity.summary,
157
+ },
158
+ };
150
159
  input.events.publish({
151
160
  event_type: "activity.event",
152
161
  payload: {
@@ -154,7 +163,7 @@ export function publishProcedureTransition(input) {
154
163
  event: {
155
164
  event_ref: createEventRef(),
156
165
  activity_ref: input.state.activity.activity_ref,
157
- event_kind: activityEventKind(input.state.activity),
166
+ event_kind: activityEventKind(input.state.activity, input.state.transition_kind),
158
167
  occurred_at: input.now().toISOString(),
159
168
  summary: input.state.activity.summary,
160
169
  ...(eventPayload === undefined ? {} : { payload: eventPayload }),
@@ -31,6 +31,10 @@ export function readControlPlaneExecutionStatus(options) {
31
31
  else if (activeProcedureActivity !== undefined) {
32
32
  status.active_activity = activeProcedureActivity;
33
33
  }
34
+ const runtime = activeRunStatus.runtime ?? activeStatus.runtime;
35
+ if (runtime !== undefined) {
36
+ status.runtime = runtime;
37
+ }
34
38
  appendActiveActivities(status, activeActivities);
35
39
  const lastConclusion = activeRunStatus.last_conclusion ?? activeStatus.last_conclusion;
36
40
  if (lastConclusion !== undefined) {
@@ -1,5 +1,6 @@
1
1
  import type { ActivityRef, WorkflowInvocationRef } from "@tutti/shared/ids";
2
2
  import type { ScratchpadProjection } from "@tutti/shared/schemas/api";
3
+ import type { ProcedureProgressUpdate } from "../procedure-engine/index.js";
3
4
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
4
5
  import type { InitializeProjectDocsResult, ProjectDocsInitializationContext, ProjectDocsInitializationDocument } from "../workspace-ops/index.js";
5
6
  import type { ControlPlaneLogger, Phase5ControlPlaneOptions } from "./types.js";
@@ -20,6 +21,7 @@ export declare function tryProjectContextBootstrapDocuments(input: {
20
21
  workflowRef: WorkflowInvocationRef;
21
22
  activityRef: ActivityRef;
22
23
  logger?: ControlPlaneLogger | undefined;
24
+ reportProgress?: (progress: ProcedureProgressUpdate) => void;
23
25
  now: () => Date;
24
26
  }): Promise<ProjectDocsInitializationDocument[] | undefined>;
25
27
  export declare function applyProjectDocsInitializationResult(input: {
@@ -65,6 +65,7 @@ export async function tryProjectContextBootstrapDocuments(input) {
65
65
  const output = await input.runner.runProjectContextBootstrap(workflowInput, {
66
66
  workflow_ref: input.workflowRef,
67
67
  activity_ref: input.activityRef,
68
+ ...(input.reportProgress === undefined ? {} : { reportProgress: input.reportProgress }),
68
69
  });
69
70
  input.logger?.info({
70
71
  workflow_ref: input.workflowRef,
@@ -81,6 +81,9 @@ async function runReferenceSummaryRefresh(options) {
81
81
  }, {
82
82
  workflow_ref: options.workflowRef,
83
83
  activity_ref: options.activityRef,
84
+ ...(options.reportProgress === undefined
85
+ ? {}
86
+ : { reportProgress: options.reportProgress }),
84
87
  });
85
88
  if (output === undefined) {
86
89
  adapterUnavailableCount += 1;
@@ -308,6 +311,7 @@ export function startReferenceSummaryRefreshProcedure(input) {
308
311
  projectContext,
309
312
  events: input.events,
310
313
  logger: input.logger,
314
+ reportProgress: context.reportProgress,
311
315
  now: input.now,
312
316
  }),
313
317
  });
@@ -91,6 +91,7 @@ export function startControlPlaneRunForTask(input) {
91
91
  on_stage: (progress) => context.reportProgress({
92
92
  phase: progress.stage,
93
93
  summary: runStageSummary(input.run, progress),
94
+ ...(progress.runtime === undefined ? {} : { runtime: progress.runtime }),
94
95
  }),
95
96
  });
96
97
  const resultRecord = recordRunPipelineResult({
@@ -48,6 +48,7 @@ export function startControlPlaneTaskCompileContinuation(options) {
48
48
  }, {
49
49
  workflow_ref: workflowRef,
50
50
  activity_ref: context.activity_ref,
51
+ reportProgress: context.reportProgress,
51
52
  });
52
53
  const conclusion = applyTaskCompileProcedureOutput({
53
54
  store: options.store,
@@ -1,6 +1,6 @@
1
1
  import { type ActivityRef, type WorkflowInvocationRef } from "@tutti/shared/ids";
2
2
  import type { SubmitWorklistDisposition, SubmitWorklistPayload, SubmitWorklistResult } from "@tutti/shared/schemas/api";
3
- import type { ProcedureEngine, ProcedureEngineState } from "../procedure-engine/index.js";
3
+ import type { ProcedureEngine, ProcedureEngineState, ProcedureProgressUpdate } from "../procedure-engine/index.js";
4
4
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
5
5
  import type { HostProjectStore } from "../store/index.js";
6
6
  import type { ControlPlaneCommandResult, ControlPlaneLogger, Phase5ControlPlaneOptions } from "./types.js";
@@ -25,6 +25,7 @@ export declare function runControlPlaneTaskCompileProcedure(options: {
25
25
  events: WorkspaceEventBus;
26
26
  now: () => Date;
27
27
  onTasksAdded: () => void;
28
+ reportProgress?: (progress: ProcedureProgressUpdate) => void;
28
29
  }): Promise<{
29
30
  kind: "needs_human";
30
31
  summary: string;
@@ -88,6 +88,7 @@ function startControlPlaneTaskCompileActivity(options) {
88
88
  events: options.events,
89
89
  workflowRef,
90
90
  activityRef: context.activity_ref,
91
+ reportProgress: context.reportProgress,
91
92
  now: options.now,
92
93
  onTasksAdded: options.onTasksAdded,
93
94
  }),
@@ -212,6 +213,7 @@ function startProjectContextBootstrapActivity(options) {
212
213
  workflowRef,
213
214
  activityRef: context.activity_ref,
214
215
  logger: options.logger,
216
+ reportProgress: context.reportProgress,
215
217
  now: options.now,
216
218
  });
217
219
  const initialized = initializeProjectDocs({
@@ -266,6 +268,9 @@ export async function runControlPlaneTaskCompileProcedure(options) {
266
268
  }, {
267
269
  workflow_ref: options.workflowRef,
268
270
  activity_ref: options.activityRef,
271
+ ...(options.reportProgress === undefined
272
+ ? {}
273
+ : { reportProgress: options.reportProgress }),
269
274
  });
270
275
  return applyTaskCompileProcedureOutput({
271
276
  store: options.store,
@@ -1,7 +1,7 @@
1
1
  import type { AccountAvatarProjection, RunLineage } from "@tutti/shared/domain";
2
2
  import type { RuntimeLogger } from "@tutti/shared/utils";
3
3
  import type { ActivityRef, ClarificationRoundRef, TaskId, TaskModuleId } from "@tutti/shared/ids";
4
- import type { ActiveApprovalProjection, ClarificationRequestPayload, MessageProjection, RecoverySummaryProjection, TaskDetailProjection, TaskProjection } from "@tutti/shared/schemas/api";
4
+ import type { ActiveApprovalProjection, ClarificationRequestPayload, ExecutionRuntimeSnapshot, MessageProjection, RecoverySummaryProjection, TaskDetailProjection, TaskProjection } from "@tutti/shared/schemas/api";
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 { CleanupStaleRunWorkspacesResult } from "../workspace-ops/index.js";
@@ -102,6 +102,7 @@ export type RunPipelineInput = {
102
102
  export type RunPipelineStageProgress = {
103
103
  stage: string;
104
104
  summary: string;
105
+ runtime?: ExecutionRuntimeSnapshot;
105
106
  };
106
107
  export type RunPipelineTimelineEvent = {
107
108
  event_kind: "promotion_reconcile";
@@ -44,6 +44,21 @@ function readOnlyAppServerOptions(options, prompt, outputSchema, skillSelectionN
44
44
  : { agentContext: options.readOnlyAppServer.agentContext }),
45
45
  };
46
46
  }
47
+ function readOnlyRuntimeTelemetry(context, stageId) {
48
+ if (context?.reportProgress === undefined) {
49
+ return {};
50
+ }
51
+ return {
52
+ runtimeTelemetry: {
53
+ activityRef: context.activity_ref,
54
+ stageId,
55
+ onSnapshot: (snapshot) => context.reportProgress?.({
56
+ phase: stageId,
57
+ runtime: snapshot,
58
+ }),
59
+ },
60
+ };
61
+ }
47
62
  function referenceSummarySourcePath(options, input) {
48
63
  if (options.readOnlyAppServer === undefined) {
49
64
  throw new Error("Codex app-server read-only Procedure adapter is not configured");
@@ -266,6 +281,7 @@ export function createOpenAiProcedureWorkflowRunner(options) {
266
281
  });
267
282
  const result = await runCodexAppServerReadOnlyProcedure({
268
283
  ...readOnlyAppServerOptions(options, instructions, TASK_COMPILE_OUTPUT_SCHEMA, "task_compile"),
284
+ ...readOnlyRuntimeTelemetry(context, "task_compile"),
269
285
  validateOutput: isTaskCompileStructuredOutput,
270
286
  networkAccess: false,
271
287
  webSearchMode: "disabled",
@@ -314,6 +330,7 @@ export function createOpenAiProcedureWorkflowRunner(options) {
314
330
  });
315
331
  const result = await runCodexAppServerReadOnlyProcedure({
316
332
  ...readOnlyAppServerOptions(options, instructions, PROJECT_CONTEXT_BOOTSTRAP_OUTPUT_SCHEMA, "project_context_bootstrap"),
333
+ ...readOnlyRuntimeTelemetry(context, "project_context_bootstrap"),
317
334
  validateOutput: isProjectContextBootstrapWorkflowOutput,
318
335
  });
319
336
  recordUsage(options, "project_context_bootstrap", result, {
@@ -329,6 +346,7 @@ export function createOpenAiProcedureWorkflowRunner(options) {
329
346
  });
330
347
  const result = await runCodexAppServerReadOnlyProcedure({
331
348
  ...readOnlyAppServerOptions(options, instructions, CONTEXT_SYNC_OUTPUT_SCHEMA, "context_sync"),
349
+ ...readOnlyRuntimeTelemetry(context, "context_sync"),
332
350
  validateOutput: isContextSyncWorkflowOutput,
333
351
  });
334
352
  recordUsage(options, "context_sync", result, {
@@ -350,6 +368,7 @@ export function createOpenAiProcedureWorkflowRunner(options) {
350
368
  try {
351
369
  const result = await runCodexAppServerReadOnlyProcedure({
352
370
  ...readOnlyAppServerOptions(options, instructions, REFERENCE_FILE_SUMMARY_OUTPUT_SCHEMA, "reference_summary_refresh"),
371
+ ...readOnlyRuntimeTelemetry(context, "reference_summary_refresh"),
353
372
  appServerCwd: workspace.cwd,
354
373
  validateOutput: isReferenceFileSummaryStructuredOutput,
355
374
  readablePaths: [workspace.cwd],
@@ -1,4 +1,4 @@
1
- import type { MessageProjection, RunResultProjection, ScratchpadProjection, TaskDetailProjection, WorklistProjection } from "@tutti/shared/schemas/api";
1
+ import type { ExecutionRuntimeSnapshot, MessageProjection, RunResultProjection, ScratchpadProjection, TaskDetailProjection, WorklistProjection } from "@tutti/shared/schemas/api";
2
2
  import type { ClarificationRequestPayload, TaskContractProjection } from "@tutti/shared/schemas/api";
3
3
  import type { ProjectDocKey } from "@tutti/shared/schemas/api";
4
4
  import type { ActivityRef, ClarificationRoundRef, ClarificationThreadRef, MessageId, TaskId, TaskModuleId, WorkflowInvocationRef } from "@tutti/shared/ids";
@@ -29,6 +29,11 @@ export type ScratchpadRefreshWorkflowContext = {
29
29
  export type ProcedureWorkflowRunContext = {
30
30
  workflow_ref: WorkflowInvocationRef;
31
31
  activity_ref: ActivityRef;
32
+ reportProgress?: (progress: {
33
+ summary?: string;
34
+ phase?: string;
35
+ runtime?: ExecutionRuntimeSnapshot;
36
+ }) => void;
32
37
  };
33
38
  export type ScratchpadRefreshWorkflowOutput = {
34
39
  decision: "ready";
@@ -1,6 +1,6 @@
1
1
  import { type ActivityRef, type WorkflowInvocationRef } from "@tutti/shared/ids";
2
2
  import type { ResultAnchor, WorkflowKind } from "@tutti/shared/domain";
3
- import type { ActivityProjection, ExecutionStatusProjection, ProcedureLane } from "@tutti/shared/schemas/api";
3
+ import type { ActivityProjection, ExecutionRuntimeSnapshot, ExecutionStatusProjection, ProcedureLane } from "@tutti/shared/schemas/api";
4
4
  export type ProcedureTerminalConclusion = {
5
5
  kind: "completed" | "failed" | "needs_human";
6
6
  summary: string;
@@ -11,6 +11,12 @@ export type ProcedureExecutionContext = {
11
11
  workflow_ref: WorkflowInvocationRef;
12
12
  workflow_kind: WorkflowKind;
13
13
  lane: ProcedureLane;
14
+ reportProgress: (progress: ProcedureProgressUpdate) => void;
15
+ };
16
+ export type ProcedureProgressUpdate = {
17
+ summary?: string;
18
+ phase?: string;
19
+ runtime?: ExecutionRuntimeSnapshot;
14
20
  };
15
21
  export type StartProcedureOptions = {
16
22
  workflow_ref: WorkflowInvocationRef;
@@ -36,6 +42,8 @@ export type StartProcedureResult = {
36
42
  export type ProcedureEngineState = {
37
43
  activity: ActivityProjection;
38
44
  execution_status: ExecutionStatusProjection;
45
+ transition_kind: "started" | "progress" | "terminal";
46
+ progress?: ProcedureProgressUpdate;
39
47
  };
40
48
  export declare class ProcedureEngine {
41
49
  private readonly activeByLane;
@@ -49,6 +57,7 @@ export declare class ProcedureEngine {
49
57
  getExecutionStatus(): ExecutionStatusProjection;
50
58
  waitForIdle(): Promise<void>;
51
59
  private stateForActivity;
60
+ private updateActiveProgress;
52
61
  private finishActive;
53
62
  private primaryActiveProcedure;
54
63
  private orderedActiveProcedures;
@@ -39,6 +39,7 @@ export class ProcedureEngine {
39
39
  workflow_ref: options.workflow_ref,
40
40
  workflow_kind: options.workflow_kind,
41
41
  lane,
42
+ reportProgress: (progress) => this.updateActiveProgress(activityRef, lane, progress, options.onTransition),
42
43
  };
43
44
  const run = async () => {
44
45
  try {
@@ -60,7 +61,7 @@ export class ProcedureEngine {
60
61
  };
61
62
  const promise = Promise.resolve().then(run);
62
63
  this.activeByLane.set(lane, { activity, lane, promise });
63
- options.onTransition?.(this.stateForActivity(activity));
64
+ options.onTransition?.(this.stateForActivity(activity, "started"));
64
65
  return {
65
66
  kind: "accepted",
66
67
  activity,
@@ -73,13 +74,17 @@ export class ProcedureEngine {
73
74
  const active = this.primaryActiveProcedure();
74
75
  if (active !== null) {
75
76
  const activeActivities = this.orderedActiveProcedures().map((entry) => executionActivityFromProcedure(entry.activity));
76
- return {
77
+ const status = {
77
78
  status: "running",
78
79
  summary: active.activity.summary,
79
80
  active_activity: executionActivityFromProcedure(active.activity),
80
81
  active_activities: activeActivities,
81
82
  updated_at: this.now().toISOString(),
82
83
  };
84
+ if (active.runtime !== undefined) {
85
+ status.runtime = active.runtime;
86
+ }
87
+ return status;
83
88
  }
84
89
  const idle = {
85
90
  status: "idle",
@@ -96,11 +101,35 @@ export class ProcedureEngine {
96
101
  await Promise.all(this.orderedActiveProcedures().map((active) => active.promise));
97
102
  }
98
103
  }
99
- stateForActivity(activity) {
100
- return {
104
+ stateForActivity(activity, transitionKind, progress) {
105
+ const state = {
101
106
  activity,
102
107
  execution_status: this.getExecutionStatus(),
108
+ transition_kind: transitionKind,
103
109
  };
110
+ if (progress !== undefined) {
111
+ state.progress = progress;
112
+ }
113
+ return state;
114
+ }
115
+ updateActiveProgress(activityRef, lane, progress, onTransition) {
116
+ const active = this.activeByLane.get(lane);
117
+ if (active === undefined || active.activity.activity_ref !== activityRef) {
118
+ return;
119
+ }
120
+ if (progress.summary !== undefined) {
121
+ active.activity.summary = progress.summary;
122
+ }
123
+ if (progress.runtime !== undefined) {
124
+ active.runtime = progress.runtime;
125
+ }
126
+ else if (progress.phase !== undefined && active.runtime !== undefined) {
127
+ active.runtime = {
128
+ ...active.runtime,
129
+ stage_id: progress.phase,
130
+ };
131
+ }
132
+ onTransition?.(this.stateForActivity(active.activity, "progress", progress));
104
133
  }
105
134
  finishActive(activityRef, lane, conclusion, onTransition) {
106
135
  const active = this.activeByLane.get(lane);
@@ -132,6 +161,7 @@ export class ProcedureEngine {
132
161
  onTransition?.({
133
162
  activity: active.activity,
134
163
  execution_status: this.getExecutionStatus(),
164
+ transition_kind: "terminal",
135
165
  });
136
166
  }
137
167
  primaryActiveProcedure() {
@@ -23,12 +23,17 @@ export type CodexAppServerJsonRpcError = {
23
23
  message: string;
24
24
  data?: unknown;
25
25
  };
26
+ export type CodexAppServerJsonRpcClientOptions = {
27
+ onNotification?: (notification: CodexAppServerJsonRpcNotification) => void;
28
+ onServerRequest?: (request: CodexAppServerJsonRpcServerRequest) => void;
29
+ };
26
30
  export declare class CodexAppServerProtocolError extends Error {
27
31
  readonly code: string;
28
32
  constructor(message: string, code: string);
29
33
  }
30
34
  export declare class CodexAppServerJsonRpcClient {
31
35
  private readonly child;
36
+ private readonly options;
32
37
  private readonly decoder;
33
38
  private readonly pendingRequests;
34
39
  private readonly notificationWaiters;
@@ -42,7 +47,7 @@ export declare class CodexAppServerJsonRpcClient {
42
47
  private readonly notificationList;
43
48
  private readonly serverRequestList;
44
49
  private readonly unresolvedServerRequestIds;
45
- constructor(child: ChildProcessWithoutNullStreams);
50
+ constructor(child: ChildProcessWithoutNullStreams, options?: CodexAppServerJsonRpcClientOptions);
46
51
  get notifications(): readonly CodexAppServerJsonRpcNotification[];
47
52
  get serverRequests(): readonly CodexAppServerJsonRpcServerRequest[];
48
53
  get stderr(): string;
@@ -23,6 +23,7 @@ function normalizeProtocolError(method, error) {
23
23
  }
24
24
  export class CodexAppServerJsonRpcClient {
25
25
  child;
26
+ options;
26
27
  decoder = new StringDecoder("utf8");
27
28
  pendingRequests = new Map();
28
29
  notificationWaiters = new Set();
@@ -36,8 +37,9 @@ export class CodexAppServerJsonRpcClient {
36
37
  notificationList = [];
37
38
  serverRequestList = [];
38
39
  unresolvedServerRequestIds = new Set();
39
- constructor(child) {
40
+ constructor(child, options = {}) {
40
41
  this.child = child;
42
+ this.options = options;
41
43
  child.stdout.on("data", (chunk) => this.handleStdout(chunk));
42
44
  child.stderr.on("data", (chunk) => {
43
45
  this.recordActivity();
@@ -272,6 +274,12 @@ export class CodexAppServerJsonRpcClient {
272
274
  recordNotification(notification) {
273
275
  this.recordActivity();
274
276
  this.notificationList.push(notification);
277
+ try {
278
+ this.options.onNotification?.(notification);
279
+ }
280
+ catch {
281
+ // Runtime observers must not affect JSON-RPC delivery.
282
+ }
275
283
  for (const waiter of Array.from(this.notificationWaiters)) {
276
284
  if (!waiter.predicate(notification)) {
277
285
  continue;
@@ -285,6 +293,12 @@ export class CodexAppServerJsonRpcClient {
285
293
  this.recordActivity();
286
294
  this.serverRequestList.push(request);
287
295
  this.unresolvedServerRequestIds.add(request.id);
296
+ try {
297
+ this.options.onServerRequest?.(request);
298
+ }
299
+ catch {
300
+ // Runtime observers must not affect interactive request handling.
301
+ }
288
302
  for (const waiter of Array.from(this.serverRequestWaiters)) {
289
303
  clearTimeout(waiter.timer);
290
304
  this.serverRequestWaiters.delete(waiter);
@@ -1,3 +1,4 @@
1
+ import { type CodexAppServerRuntimeTelemetryOptions } from "./runtime-telemetry.js";
1
2
  import { type CodexAppServerAgentContextRuntime, type CodexAppServerSkillSelectionName } from "./skills.js";
2
3
  import { type OpenAiTokenUsage } from "../token-usage.js";
3
4
  export type CodexAppServerWebSearchMode = "disabled" | "cached" | "live";
@@ -22,6 +23,7 @@ export type CodexAppServerReadOnlyProcedureInput<TOutput> = {
22
23
  requestTimeoutMs?: number;
23
24
  turnTimeoutMs?: number;
24
25
  baseEnv?: NodeJS.ProcessEnv;
26
+ runtimeTelemetry?: CodexAppServerRuntimeTelemetryOptions;
25
27
  };
26
28
  export type CodexAppServerReadOnlyProcedureResult<TOutput> = {
27
29
  output: TOutput;
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { buildCodexAppServerProcessPlan } from "../codex-app-server.js";
6
6
  import { DEFAULT_OPENAI_MODEL } from "../model-config.js";
7
7
  import { CodexAppServerJsonRpcClient, CodexAppServerProtocolError, } from "./json-rpc.js";
8
+ import { createCodexAppServerRuntimeObserver, } from "./runtime-telemetry.js";
8
9
  import { createReadOnlySandboxPolicy } from "./sandbox-policy.js";
9
10
  import { buildCodexAppServerProviderConfigOverrides, resolveCodexAppServerModelProvider, } from "./provider-config.js";
10
11
  import { buildCodexAppServerTurnInput, resolveCodexAppServerContextRuntimeHint, resolveCodexAppServerSkillInputs, CodexAppServerSkillSelectionError, } from "./skills.js";
@@ -125,7 +126,17 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
125
126
  env: createCodexSpawnEnv(codexHome, options.baseEnv ?? process.env),
126
127
  stdio: "pipe",
127
128
  });
128
- const client = new CodexAppServerJsonRpcClient(child);
129
+ const runtimeObserver = options.runtimeTelemetry === undefined
130
+ ? undefined
131
+ : createCodexAppServerRuntimeObserver(options.runtimeTelemetry);
132
+ const client = new CodexAppServerJsonRpcClient(child, {
133
+ ...(runtimeObserver === undefined
134
+ ? {}
135
+ : {
136
+ onNotification: runtimeObserver.notification,
137
+ onServerRequest: runtimeObserver.serverRequest,
138
+ }),
139
+ });
129
140
  const sensitiveValues = [
130
141
  options.apiKey,
131
142
  tempRoot,
@@ -284,6 +295,7 @@ export async function runCodexAppServerReadOnlyProcedure(options) {
284
295
  throw new CodexAppServerReadOnlyProcedureError("app_server_protocol_error", redactWithKnownValues(message, sensitiveValues));
285
296
  }
286
297
  finally {
298
+ runtimeObserver?.flush();
287
299
  client.dispose();
288
300
  rmSync(tempRoot, { recursive: true, force: true });
289
301
  }
@@ -0,0 +1,18 @@
1
+ import type { ActivityRef } from "@tutti/shared/ids";
2
+ import type { ExecutionRuntimeSnapshot } from "@tutti/shared/schemas/api";
3
+ import type { CodexAppServerJsonRpcNotification, CodexAppServerJsonRpcServerRequest } from "./json-rpc.js";
4
+ export type CodexAppServerRuntimeTelemetryOptions = {
5
+ activityRef: ActivityRef;
6
+ stageId?: string;
7
+ startedAt?: string;
8
+ throttleMs?: number;
9
+ now?: () => Date;
10
+ onSnapshot?: (snapshot: ExecutionRuntimeSnapshot) => void;
11
+ };
12
+ export type CodexAppServerRuntimeObserver = {
13
+ notification: (notification: CodexAppServerJsonRpcNotification) => void;
14
+ serverRequest: (request: CodexAppServerJsonRpcServerRequest) => void;
15
+ flush: () => void;
16
+ };
17
+ export declare function createCodexAppServerRuntimeObserver(options: CodexAppServerRuntimeTelemetryOptions): CodexAppServerRuntimeObserver;
18
+ //# sourceMappingURL=runtime-telemetry.d.ts.map
@@ -0,0 +1,130 @@
1
+ import { extractCodexAppServerTokenUsage } from "../token-usage.js";
2
+ const DEFAULT_THROTTLE_MS = 5_000;
3
+ function searchText(value) {
4
+ if (typeof value === "string") {
5
+ return value.toLocaleLowerCase();
6
+ }
7
+ if (typeof value !== "object" || value === null) {
8
+ return "";
9
+ }
10
+ const record = value;
11
+ const tokens = ["type", "kind", "name", "status", "method", "state"]
12
+ .map((key) => record[key])
13
+ .filter((entry) => typeof entry === "string");
14
+ return tokens.join(" ").toLocaleLowerCase();
15
+ }
16
+ function actionFromNotification(notification) {
17
+ const method = notification.method;
18
+ const paramsText = searchText(notification.params);
19
+ const combined = `${method} ${paramsText}`.toLocaleLowerCase();
20
+ if (combined.includes("commandexecution") || combined.includes("command_execution")) {
21
+ return "running_command";
22
+ }
23
+ if (combined.includes("filechange") || combined.includes("file_change")) {
24
+ return "editing_files";
25
+ }
26
+ if (combined.includes("plan")) {
27
+ return "planning";
28
+ }
29
+ if (combined.includes("mcptoolcall") ||
30
+ combined.includes("mcp_tool_call") ||
31
+ combined.includes("dynamictoolcall") ||
32
+ combined.includes("dynamic_tool_call") ||
33
+ combined.includes("websearch") ||
34
+ combined.includes("web_search")) {
35
+ return "using_tool";
36
+ }
37
+ if (combined.includes("reasoning")) {
38
+ return "reading_context";
39
+ }
40
+ if (method === "item/agentMessage/delta" || method === "item/completed") {
41
+ return "finalizing";
42
+ }
43
+ return undefined;
44
+ }
45
+ function runtimeTokenUsage(usage) {
46
+ return {
47
+ input_tokens: usage.input_tokens,
48
+ cached_input_tokens: usage.cached_input_tokens,
49
+ output_tokens: usage.output_tokens,
50
+ reasoning_output_tokens: usage.reasoning_output_tokens,
51
+ total_tokens: usage.total_tokens,
52
+ };
53
+ }
54
+ export function createCodexAppServerRuntimeObserver(options) {
55
+ let latest;
56
+ let timer;
57
+ let lastEmittedAt = 0;
58
+ const throttleMs = options.throttleMs ?? DEFAULT_THROTTLE_MS;
59
+ const now = options.now ?? (() => new Date());
60
+ const emit = () => {
61
+ if (latest === undefined || options.onSnapshot === undefined) {
62
+ return;
63
+ }
64
+ lastEmittedAt = Date.now();
65
+ options.onSnapshot(latest);
66
+ };
67
+ const scheduleEmit = () => {
68
+ if (options.onSnapshot === undefined) {
69
+ return;
70
+ }
71
+ const elapsed = Date.now() - lastEmittedAt;
72
+ if (elapsed >= throttleMs) {
73
+ if (timer !== undefined) {
74
+ clearTimeout(timer);
75
+ timer = undefined;
76
+ }
77
+ emit();
78
+ return;
79
+ }
80
+ if (timer !== undefined) {
81
+ return;
82
+ }
83
+ timer = setTimeout(() => {
84
+ timer = undefined;
85
+ emit();
86
+ }, Math.max(1, throttleMs - elapsed));
87
+ };
88
+ const update = (patch) => {
89
+ if (patch.currentAction === undefined && patch.tokenUsage === undefined) {
90
+ return;
91
+ }
92
+ const updatedAt = now().toISOString();
93
+ latest = {
94
+ activity_ref: options.activityRef,
95
+ ...(options.stageId === undefined ? {} : { stage_id: options.stageId }),
96
+ ...(options.startedAt === undefined ? {} : { started_at: options.startedAt }),
97
+ ...(latest?.token_usage === undefined ? {} : { token_usage: latest.token_usage }),
98
+ ...(latest?.current_action === undefined ? {} : { current_action: latest.current_action }),
99
+ updated_at: updatedAt,
100
+ };
101
+ if (patch.tokenUsage !== undefined) {
102
+ latest.token_usage = runtimeTokenUsage(patch.tokenUsage);
103
+ }
104
+ if (patch.currentAction !== undefined) {
105
+ latest.current_action = patch.currentAction;
106
+ }
107
+ scheduleEmit();
108
+ };
109
+ return {
110
+ notification: (notification) => {
111
+ const tokenUsage = extractCodexAppServerTokenUsage([notification]);
112
+ const currentAction = actionFromNotification(notification);
113
+ update({
114
+ ...(tokenUsage === undefined ? {} : { tokenUsage }),
115
+ ...(currentAction === undefined ? {} : { currentAction }),
116
+ });
117
+ },
118
+ serverRequest: () => {
119
+ update({ currentAction: "waiting_for_approval" });
120
+ },
121
+ flush: () => {
122
+ if (timer !== undefined) {
123
+ clearTimeout(timer);
124
+ timer = undefined;
125
+ }
126
+ emit();
127
+ },
128
+ };
129
+ }
130
+ //# sourceMappingURL=runtime-telemetry.js.map