@bastani/atomic 0.8.26-alpha.4 → 0.8.26-alpha.6
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/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/CHANGELOG.md +12 -0
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +12 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +12 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/subagents/src/runs/background/subagent-runner.ts +48 -10
- package/dist/builtin/subagents/src/runs/foreground/execution.ts +30 -9
- package/dist/builtin/subagents/src/runs/shared/final-drain.ts +34 -0
- package/dist/builtin/subagents/src/runs/shared/model-fallback.ts +416 -7
- package/dist/builtin/web-access/CHANGELOG.md +12 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +12 -0
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +6 -6
- package/dist/builtin/workflows/builtin/goal.ts +10 -10
- package/dist/builtin/workflows/builtin/open-claude-design.ts +4 -4
- package/dist/builtin/workflows/builtin/ralph.ts +16 -16
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/extension/index.ts +10 -2
- package/dist/builtin/workflows/src/extension/runtime.ts +35 -3
- package/dist/builtin/workflows/src/runs/background/status.ts +52 -6
- package/dist/builtin/workflows/src/runs/foreground/executor.ts +441 -15
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +69 -8
- package/dist/builtin/workflows/src/runs/shared/model-fallback.ts +402 -8
- package/dist/builtin/workflows/src/shared/persistence-restore.ts +182 -6
- package/dist/builtin/workflows/src/shared/persistence-session-entries.ts +76 -6
- package/dist/builtin/workflows/src/shared/stage-prompt.ts +33 -2
- package/dist/builtin/workflows/src/shared/store-types.ts +31 -0
- package/dist/builtin/workflows/src/shared/store.ts +99 -11
- package/dist/builtin/workflows/src/shared/workflow-failures.ts +758 -132
- package/dist/builtin/workflows/src/tui/store-widget-installer.ts +74 -74
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts +5 -5
- package/dist/core/tools/ask-user-question/tool/format-answer.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/format-answer.js +5 -5
- package/dist/core/tools/ask-user-question/tool/format-answer.js.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts +16 -3
- package/dist/core/tools/ask-user-question/tool/response-envelope.d.ts.map +1 -1
- package/dist/core/tools/ask-user-question/tool/response-envelope.js +21 -3
- package/dist/core/tools/ask-user-question/tool/response-envelope.js.map +1 -1
- package/package.json +1 -1
|
@@ -160,121 +160,125 @@ export function installToolExecutionHooks(pi: LiveWidgetAPI, storeInstance: Stor
|
|
|
160
160
|
const extensionOn = pi.on;
|
|
161
161
|
if (typeof eventBusOn !== "function" && typeof extensionOn !== "function") return;
|
|
162
162
|
|
|
163
|
-
|
|
163
|
+
interface StageScope {
|
|
164
|
+
readonly runId: string;
|
|
165
|
+
readonly stageId: string;
|
|
166
|
+
}
|
|
164
167
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
+
interface ActiveToolCall extends StageScope {
|
|
169
|
+
readonly callId: string;
|
|
170
|
+
readonly name: string;
|
|
171
|
+
readonly startedAt: number;
|
|
172
|
+
}
|
|
168
173
|
|
|
169
|
-
|
|
170
|
-
|
|
174
|
+
const terminalRunStatuses = new Set(["completed", "failed", "killed"]);
|
|
175
|
+
const terminalStageStatuses = new Set(["completed", "failed", "skipped"]);
|
|
176
|
+
const activeToolCalls = new Map<string, ActiveToolCall>();
|
|
171
177
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
function activeCallKey(runId: string, stageId: string, callId: string): string {
|
|
179
|
+
return `${runId}\0${stageId}\0${callId}`;
|
|
180
|
+
}
|
|
175
181
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
182
|
+
function firstNonEmptyString(...values: readonly unknown[]): string | null {
|
|
183
|
+
for (const value of values) {
|
|
184
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
179
185
|
}
|
|
180
|
-
|
|
181
186
|
return null;
|
|
182
187
|
}
|
|
183
188
|
|
|
184
|
-
function
|
|
185
|
-
|
|
189
|
+
function resolveExplicitStageScope(payload: ToolExecutionStartPayload): StageScope | null {
|
|
190
|
+
const runId = firstNonEmptyString(payload.runId, payload.run_id);
|
|
191
|
+
const stageId = firstNonEmptyString(payload.stageId, payload.stage_id);
|
|
192
|
+
if (runId === null || stageId === null) return null;
|
|
193
|
+
return { runId, stageId };
|
|
186
194
|
}
|
|
187
195
|
|
|
188
|
-
function
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
const
|
|
196
|
+
function hasLiveStageScope(scope: StageScope, snap: StoreSnapshot): boolean {
|
|
197
|
+
const run = snap.runs.find((candidate) => candidate.id === scope.runId);
|
|
198
|
+
if (!run || run.endedAt !== undefined || terminalRunStatuses.has(run.status)) return false;
|
|
199
|
+
const stage = run.stages.find((candidate) => candidate.id === scope.stageId);
|
|
200
|
+
return stage !== undefined && !terminalStageStatuses.has(stage.status);
|
|
201
|
+
}
|
|
192
202
|
|
|
193
|
-
|
|
194
|
-
|
|
203
|
+
function pruneActiveToolCalls(snap: StoreSnapshot): void {
|
|
204
|
+
for (const [key, call] of activeToolCalls) {
|
|
205
|
+
if (!hasLiveStageScope(call, snap)) activeToolCalls.delete(key);
|
|
195
206
|
}
|
|
196
|
-
|
|
197
|
-
const matches = [...activeAskUserQuestionCalls.values()].filter((entry) => {
|
|
198
|
-
if (entry.callId !== callId) return false;
|
|
199
|
-
if (runId !== undefined && entry.runId !== runId) return false;
|
|
200
|
-
if (stageId !== undefined && entry.stageId !== stageId) return false;
|
|
201
|
-
return true;
|
|
202
|
-
});
|
|
203
|
-
return matches.length === 1 ? matches[0] : undefined;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
|
-
function
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
);
|
|
210
|
-
}
|
|
209
|
+
function activeToolCallForPayload(payload: ToolExecutionStartPayload): { key: string; call: ActiveToolCall } | null {
|
|
210
|
+
const scope = resolveExplicitStageScope(payload);
|
|
211
|
+
if (!scope) return null;
|
|
211
212
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
activeAskUserQuestionCalls.set(activeCallKey(ids.runId, ids.stageId, callId), {
|
|
216
|
-
...ids,
|
|
217
|
-
callId,
|
|
218
|
-
});
|
|
219
|
-
storeInstance.recordStageAwaitingInput(ids.runId, ids.stageId, true, payload.ts);
|
|
213
|
+
const key = activeCallKey(scope.runId, scope.stageId, toolCallId(payload));
|
|
214
|
+
const call = activeToolCalls.get(key);
|
|
215
|
+
return call ? { key, call } : null;
|
|
220
216
|
}
|
|
221
217
|
|
|
222
|
-
|
|
223
|
-
const activeCall = findActiveAskCall(payload);
|
|
224
|
-
const resolvedIds = activeCall ?? ids;
|
|
225
|
-
if (resolvedIds === null || resolvedIds === undefined) return;
|
|
226
|
-
|
|
227
|
-
const shouldClear = activeCall !== undefined || isAskUserQuestionToolName(toolName(payload));
|
|
228
|
-
if (!shouldClear) return;
|
|
229
|
-
|
|
230
|
-
activeAskUserQuestionCalls.delete(activeCallKey(resolvedIds.runId, resolvedIds.stageId, toolCallId(payload)));
|
|
231
|
-
if (!stageHasActiveAskCall(resolvedIds.runId, resolvedIds.stageId)) {
|
|
232
|
-
storeInstance.recordStageAwaitingInput(resolvedIds.runId, resolvedIds.stageId, false);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
218
|
+
storeInstance.subscribe(pruneActiveToolCalls);
|
|
235
219
|
|
|
236
220
|
function recordToolStart(payload: unknown): void {
|
|
237
221
|
if (!isToolExecutionPayload(payload)) return;
|
|
238
222
|
|
|
239
|
-
const
|
|
240
|
-
|
|
223
|
+
const snap = storeInstance.snapshot();
|
|
224
|
+
pruneActiveToolCalls(snap);
|
|
225
|
+
|
|
226
|
+
const scope = resolveExplicitStageScope(payload);
|
|
227
|
+
if (!scope || !hasLiveStageScope(scope, snap)) return;
|
|
241
228
|
|
|
242
|
-
|
|
243
|
-
|
|
229
|
+
const callId = toolCallId(payload);
|
|
230
|
+
const key = activeCallKey(scope.runId, scope.stageId, callId);
|
|
231
|
+
if (activeToolCalls.has(key)) return;
|
|
232
|
+
|
|
233
|
+
const name = toolName(payload);
|
|
234
|
+
const startedAt = payload.ts ?? Date.now();
|
|
235
|
+
activeToolCalls.set(key, { ...scope, callId, name, startedAt });
|
|
236
|
+
storeInstance.recordToolStart(scope.runId, scope.stageId, {
|
|
237
|
+
name,
|
|
244
238
|
input: toolInput(payload),
|
|
245
|
-
startedAt
|
|
239
|
+
startedAt,
|
|
246
240
|
});
|
|
247
|
-
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function recordToolUpdate(payload: unknown): void {
|
|
244
|
+
if (!isToolExecutionPayload(payload)) return;
|
|
245
|
+
|
|
246
|
+
pruneActiveToolCalls(storeInstance.snapshot());
|
|
247
|
+
|
|
248
|
+
if (!activeToolCallForPayload(payload)) return;
|
|
249
|
+
// Updates are attach-only until the store has an explicit update API.
|
|
248
250
|
}
|
|
249
251
|
|
|
250
252
|
function recordToolEnd(payload: unknown): void {
|
|
251
253
|
if (!isToolExecutionPayload(payload)) return;
|
|
252
254
|
|
|
253
|
-
|
|
254
|
-
const ids = activeAskCall ?? resolveIds(payload, false);
|
|
255
|
-
if (!ids) return;
|
|
255
|
+
pruneActiveToolCalls(storeInstance.snapshot());
|
|
256
256
|
|
|
257
|
-
|
|
258
|
-
|
|
257
|
+
const active = activeToolCallForPayload(payload);
|
|
258
|
+
if (!active) return;
|
|
259
|
+
|
|
260
|
+
storeInstance.recordToolEnd(active.call.runId, active.call.stageId, {
|
|
261
|
+
name: active.call.name,
|
|
259
262
|
input: toolInput(payload),
|
|
260
|
-
startedAt:
|
|
263
|
+
startedAt: active.call.startedAt,
|
|
261
264
|
endedAt: payload.endedAt ?? payload.ended_at ?? Date.now(),
|
|
262
265
|
output: payload.output,
|
|
263
266
|
});
|
|
264
|
-
|
|
267
|
+
activeToolCalls.delete(active.key);
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
const safeStart = safelyHandle(recordToolStart);
|
|
271
|
+
const safeUpdate = safelyHandle(recordToolUpdate);
|
|
268
272
|
const safeEnd = safelyHandle(recordToolEnd);
|
|
269
273
|
|
|
270
274
|
if (typeof eventBusOn === "function") {
|
|
271
275
|
eventBusOn.call(pi.events, "tool_execution_start", safeStart);
|
|
272
|
-
eventBusOn.call(pi.events, "tool_execution_update",
|
|
276
|
+
eventBusOn.call(pi.events, "tool_execution_update", safeUpdate);
|
|
273
277
|
eventBusOn.call(pi.events, "tool_execution_end", safeEnd);
|
|
274
278
|
}
|
|
275
279
|
if (typeof extensionOn === "function") {
|
|
276
280
|
extensionOn.call(pi, "tool_execution_start", safeStart);
|
|
277
|
-
extensionOn.call(pi, "tool_execution_update",
|
|
281
|
+
extensionOn.call(pi, "tool_execution_update", safeUpdate);
|
|
278
282
|
extensionOn.call(pi, "tool_execution_end", safeEnd);
|
|
279
283
|
extensionOn.call(pi, "tool_call", safeStart);
|
|
280
284
|
extensionOn.call(pi, "tool_result", safeEnd);
|
|
@@ -306,7 +310,3 @@ function toolCallId(payload: ToolExecutionStartPayload): string {
|
|
|
306
310
|
function toolInput(payload: ToolExecutionStartPayload): Record<string, unknown> | undefined {
|
|
307
311
|
return payload.input ?? payload.args;
|
|
308
312
|
}
|
|
309
|
-
|
|
310
|
-
function isAskUserQuestionToolName(name: string): boolean {
|
|
311
|
-
return name.toLowerCase().replace(/[^a-z0-9]/g, "") === "askuserquestion";
|
|
312
|
-
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { QuestionAnswer } from "./types.ts";
|
|
2
2
|
/**
|
|
3
|
-
* Continuation message used in the LLM-facing envelope. Two-sentence
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Continuation message used in the LLM-facing envelope. Two-sentence form —
|
|
4
|
+
* the model needs the "Stop…wait…" directive to know what to do next after the
|
|
5
|
+
* user picks chat instead of answering.
|
|
6
6
|
*/
|
|
7
|
-
export declare const CHAT_CONTINUATION_MESSAGE = "User wants to chat about this.
|
|
7
|
+
export declare const CHAT_CONTINUATION_MESSAGE = "User wants to chat about this. Stop the current task flow and wait for the user's next message.";
|
|
8
8
|
/**
|
|
9
9
|
* One-sentence summary form shown in the on-screen Submit-tab review pane. The dialog
|
|
10
10
|
* already shows the question; the imperative continuation directive belongs in the
|
|
@@ -20,7 +20,7 @@ export declare const NO_INPUT_PLACEHOLDER = "(no input)";
|
|
|
20
20
|
export type FormatAnswerVariant = "summary" | "envelope";
|
|
21
21
|
/**
|
|
22
22
|
* Format a `QuestionAnswer` to its scalar string form. Variant controls only the
|
|
23
|
-
* `kind: "chat"` branch — the envelope's
|
|
23
|
+
* `kind: "chat"` branch — the envelope's stop/wait directive is needed by the LLM,
|
|
24
24
|
* the dialog summary's one-sentence reminder is not. All other branches return identical
|
|
25
25
|
* strings; the `kind: "custom"` empty-string handling and the option fallback both unify
|
|
26
26
|
* on `NO_INPUT_PLACEHOLDER`. Switch is exhaustive — non-`void` return enforces every
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format-answer.d.ts","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/tool/format-answer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,
|
|
1
|
+
{"version":3,"file":"format-answer.d.ts","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/tool/format-answer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,oGAC4D,CAAC;AAEnG;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,kCAAkC,CAAC;AAEpE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,eAAe,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,CAAC;AAEzD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,EAAE,mBAAmB,GAAG,MAAM,CAW1F","sourcesContent":["import type { QuestionAnswer } from \"./types.ts\";\n\n/**\n * Continuation message used in the LLM-facing envelope. Two-sentence form —\n * the model needs the \"Stop…wait…\" directive to know what to do next after the\n * user picks chat instead of answering.\n */\nexport const CHAT_CONTINUATION_MESSAGE =\n\t\"User wants to chat about this. Stop the current task flow and wait for the user's next message.\";\n\n/**\n * One-sentence summary form shown in the on-screen Submit-tab review pane. The dialog\n * already shows the question; the imperative continuation directive belongs in the\n * envelope, not in the user-facing summary box.\n */\nexport const CHAT_SUMMARY_MESSAGE = \"User wants to chat about this\";\n\n/**\n * Placeholder for empty / null answer text. Used uniformly across both variants — the\n * earlier `(no answer)` fallback in the dialog summary was accidental drift; tests pin\n * `(no input)` only.\n */\nexport const NO_INPUT_PLACEHOLDER = \"(no input)\";\n\nexport type FormatAnswerVariant = \"summary\" | \"envelope\";\n\n/**\n * Format a `QuestionAnswer` to its scalar string form. Variant controls only the\n * `kind: \"chat\"` branch — the envelope's stop/wait directive is needed by the LLM,\n * the dialog summary's one-sentence reminder is not. All other branches return identical\n * strings; the `kind: \"custom\"` empty-string handling and the option fallback both unify\n * on `NO_INPUT_PLACEHOLDER`. Switch is exhaustive — non-`void` return enforces every\n * variant is handled.\n */\nexport function formatAnswerScalar(a: QuestionAnswer, variant: FormatAnswerVariant): string {\n\tswitch (a.kind) {\n\t\tcase \"chat\":\n\t\t\treturn variant === \"envelope\" ? CHAT_CONTINUATION_MESSAGE : CHAT_SUMMARY_MESSAGE;\n\t\tcase \"multi\":\n\t\t\treturn a.selected && a.selected.length > 0 ? a.selected.join(\", \") : NO_INPUT_PLACEHOLDER;\n\t\tcase \"custom\":\n\t\t\treturn a.answer && a.answer.length > 0 ? a.answer : NO_INPUT_PLACEHOLDER;\n\t\tcase \"option\":\n\t\t\treturn a.answer ?? NO_INPUT_PLACEHOLDER;\n\t}\n}\n"]}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Continuation message used in the LLM-facing envelope. Two-sentence
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Continuation message used in the LLM-facing envelope. Two-sentence form —
|
|
3
|
+
* the model needs the "Stop…wait…" directive to know what to do next after the
|
|
4
|
+
* user picks chat instead of answering.
|
|
5
5
|
*/
|
|
6
|
-
export const CHAT_CONTINUATION_MESSAGE = "User wants to chat about this.
|
|
6
|
+
export const CHAT_CONTINUATION_MESSAGE = "User wants to chat about this. Stop the current task flow and wait for the user's next message.";
|
|
7
7
|
/**
|
|
8
8
|
* One-sentence summary form shown in the on-screen Submit-tab review pane. The dialog
|
|
9
9
|
* already shows the question; the imperative continuation directive belongs in the
|
|
@@ -18,7 +18,7 @@ export const CHAT_SUMMARY_MESSAGE = "User wants to chat about this";
|
|
|
18
18
|
export const NO_INPUT_PLACEHOLDER = "(no input)";
|
|
19
19
|
/**
|
|
20
20
|
* Format a `QuestionAnswer` to its scalar string form. Variant controls only the
|
|
21
|
-
* `kind: "chat"` branch — the envelope's
|
|
21
|
+
* `kind: "chat"` branch — the envelope's stop/wait directive is needed by the LLM,
|
|
22
22
|
* the dialog summary's one-sentence reminder is not. All other branches return identical
|
|
23
23
|
* strings; the `kind: "custom"` empty-string handling and the option fallback both unify
|
|
24
24
|
* on `NO_INPUT_PLACEHOLDER`. Switch is exhaustive — non-`void` return enforces every
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format-answer.js","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/tool/format-answer.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GACrC
|
|
1
|
+
{"version":3,"file":"format-answer.js","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/tool/format-answer.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,CAAC,MAAM,yBAAyB,GACrC,iGAAiG,CAAC;AAEnG;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,+BAA+B,CAAC;AAEpE;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,YAAY,CAAC;AAIjD;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,CAAiB,EAAE,OAA4B;IACjF,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAC;QAChB,KAAK,MAAM;YACV,OAAO,OAAO,KAAK,UAAU,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,oBAAoB,CAAC;QAClF,KAAK,OAAO;YACX,OAAO,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC;QAC3F,KAAK,QAAQ;YACZ,OAAO,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,oBAAoB,CAAC;QAC1E,KAAK,QAAQ;YACZ,OAAO,CAAC,CAAC,MAAM,IAAI,oBAAoB,CAAC;IAC1C,CAAC;AACF,CAAC","sourcesContent":["import type { QuestionAnswer } from \"./types.ts\";\n\n/**\n * Continuation message used in the LLM-facing envelope. Two-sentence form —\n * the model needs the \"Stop…wait…\" directive to know what to do next after the\n * user picks chat instead of answering.\n */\nexport const CHAT_CONTINUATION_MESSAGE =\n\t\"User wants to chat about this. Stop the current task flow and wait for the user's next message.\";\n\n/**\n * One-sentence summary form shown in the on-screen Submit-tab review pane. The dialog\n * already shows the question; the imperative continuation directive belongs in the\n * envelope, not in the user-facing summary box.\n */\nexport const CHAT_SUMMARY_MESSAGE = \"User wants to chat about this\";\n\n/**\n * Placeholder for empty / null answer text. Used uniformly across both variants — the\n * earlier `(no answer)` fallback in the dialog summary was accidental drift; tests pin\n * `(no input)` only.\n */\nexport const NO_INPUT_PLACEHOLDER = \"(no input)\";\n\nexport type FormatAnswerVariant = \"summary\" | \"envelope\";\n\n/**\n * Format a `QuestionAnswer` to its scalar string form. Variant controls only the\n * `kind: \"chat\"` branch — the envelope's stop/wait directive is needed by the LLM,\n * the dialog summary's one-sentence reminder is not. All other branches return identical\n * strings; the `kind: \"custom\"` empty-string handling and the option fallback both unify\n * on `NO_INPUT_PLACEHOLDER`. Switch is exhaustive — non-`void` return enforces every\n * variant is handled.\n */\nexport function formatAnswerScalar(a: QuestionAnswer, variant: FormatAnswerVariant): string {\n\tswitch (a.kind) {\n\t\tcase \"chat\":\n\t\t\treturn variant === \"envelope\" ? CHAT_CONTINUATION_MESSAGE : CHAT_SUMMARY_MESSAGE;\n\t\tcase \"multi\":\n\t\t\treturn a.selected && a.selected.length > 0 ? a.selected.join(\", \") : NO_INPUT_PLACEHOLDER;\n\t\tcase \"custom\":\n\t\t\treturn a.answer && a.answer.length > 0 ? a.answer : NO_INPUT_PLACEHOLDER;\n\t\tcase \"option\":\n\t\t\treturn a.answer ?? NO_INPUT_PLACEHOLDER;\n\t}\n}\n"]}
|
|
@@ -2,10 +2,19 @@ import type { QuestionAnswer, QuestionnaireResult, QuestionParams } from "./type
|
|
|
2
2
|
export declare const DECLINE_MESSAGE = "User declined to answer questions";
|
|
3
3
|
export declare const ENVELOPE_PREFIX = "User has answered your questions:";
|
|
4
4
|
export declare const ENVELOPE_SUFFIX = "You can now continue with the user's answers in mind.";
|
|
5
|
+
/**
|
|
6
|
+
* True when any answer in the result carries `kind: "chat"`.
|
|
7
|
+
* Used by `buildQuestionnaireResponse` to switch to the terminate path.
|
|
8
|
+
*/
|
|
9
|
+
export declare function hasChatAnswer(result: QuestionnaireResult): boolean;
|
|
5
10
|
/**
|
|
6
11
|
* Map a `QuestionnaireResult` (or null/cancelled) to the LLM-facing tool envelope.
|
|
7
|
-
* Pure of `(result, params)`; cancelled and "no segments" both fall to
|
|
8
|
-
* so the model sees a single canonical "didn't answer" signal
|
|
12
|
+
* Pure of `(result, params)`; cancelled and non-chat "no segments" both fall to
|
|
13
|
+
* `DECLINE_MESSAGE` so the model sees a single canonical "didn't answer" signal
|
|
14
|
+
* regardless of why.
|
|
15
|
+
*
|
|
16
|
+
* Chat rule: when any non-cancelled answer is `kind: "chat"`, the result carries
|
|
17
|
+
* `terminate: true` and stop/wait wording instead of the generic continuation suffix.
|
|
9
18
|
*/
|
|
10
19
|
export declare function buildQuestionnaireResponse(result: QuestionnaireResult | null | undefined, params: QuestionParams): {
|
|
11
20
|
content: {
|
|
@@ -13,17 +22,21 @@ export declare function buildQuestionnaireResponse(result: QuestionnaireResult |
|
|
|
13
22
|
text: string;
|
|
14
23
|
}[];
|
|
15
24
|
details: QuestionnaireResult;
|
|
25
|
+
terminate?: boolean | undefined;
|
|
16
26
|
};
|
|
17
27
|
/**
|
|
18
28
|
* Format a single answer segment for the envelope. Pure of `a`. The `"Q"="A"` shape and
|
|
19
29
|
* the optional `selected preview:` / `user notes:` suffixes are pinned by envelope tests.
|
|
20
30
|
*/
|
|
21
31
|
export declare function buildAnswerSegment(a: QuestionAnswer): string;
|
|
22
|
-
export declare function buildToolResult(text: string, details: QuestionnaireResult
|
|
32
|
+
export declare function buildToolResult(text: string, details: QuestionnaireResult, options?: {
|
|
33
|
+
terminate?: boolean;
|
|
34
|
+
}): {
|
|
23
35
|
content: {
|
|
24
36
|
type: "text";
|
|
25
37
|
text: string;
|
|
26
38
|
}[];
|
|
27
39
|
details: QuestionnaireResult;
|
|
40
|
+
terminate?: boolean | undefined;
|
|
28
41
|
};
|
|
29
42
|
//# sourceMappingURL=response-envelope.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response-envelope.d.ts","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/tool/response-envelope.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEtF,eAAO,MAAM,eAAe,sCAAsC,CAAC;AACnE,eAAO,MAAM,eAAe,sCAAsC,CAAC;AACnE,eAAO,MAAM,eAAe,0DAA0D,CAAC;
|
|
1
|
+
{"version":3,"file":"response-envelope.d.ts","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/tool/response-envelope.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,cAAc,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEtF,eAAO,MAAM,eAAe,sCAAsC,CAAC;AACnE,eAAO,MAAM,eAAe,sCAAsC,CAAC;AACnE,eAAO,MAAM,eAAe,0DAA0D,CAAC;AAIvF;;;GAGG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAElE;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,mBAAmB,GAAG,IAAI,GAAG,SAAS,EAAE,MAAM,EAAE,cAAc;;cAoC7F,MAAM;;;;;EAfzB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,CAAC,EAAE,cAAc,GAAG,MAAM,CAK5D;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,mBAAmB,EAAE,OAAO,CAAC,EAAE;IAAE,SAAS,CAAC,EAAE,OAAO,CAAA;CAAE;;cAEzF,MAAM;;;;;EAIzB","sourcesContent":["import { formatAnswerScalar } from \"./format-answer.ts\";\nimport type { QuestionAnswer, QuestionnaireResult, QuestionParams } from \"./types.ts\";\n\nexport const DECLINE_MESSAGE = \"User declined to answer questions\";\nexport const ENVELOPE_PREFIX = \"User has answered your questions:\";\nexport const ENVELOPE_SUFFIX = \"You can now continue with the user's answers in mind.\";\nconst CHAT_TERMINATION_DIRECTIVE =\n\t\"User wants to chat about this before choosing. Stop the current task flow and wait for the user's next message.\";\n\n/**\n * True when any answer in the result carries `kind: \"chat\"`.\n * Used by `buildQuestionnaireResponse` to switch to the terminate path.\n */\nexport function hasChatAnswer(result: QuestionnaireResult): boolean {\n\treturn result.answers.some((a) => a.kind === \"chat\");\n}\n\n/**\n * Map a `QuestionnaireResult` (or null/cancelled) to the LLM-facing tool envelope.\n * Pure of `(result, params)`; cancelled and non-chat \"no segments\" both fall to\n * `DECLINE_MESSAGE` so the model sees a single canonical \"didn't answer\" signal\n * regardless of why.\n *\n * Chat rule: when any non-cancelled answer is `kind: \"chat\"`, the result carries\n * `terminate: true` and stop/wait wording instead of the generic continuation suffix.\n */\nexport function buildQuestionnaireResponse(result: QuestionnaireResult | null | undefined, params: QuestionParams) {\n\tif (!result || result.cancelled) {\n\t\treturn buildToolResult(DECLINE_MESSAGE, {\n\t\t\tanswers: result?.answers ?? [],\n\t\t\tcancelled: true,\n\t\t});\n\t}\n\tconst containsChatAnswer = hasChatAnswer(result);\n\tconst segments: string[] = [];\n\tfor (let i = 0; i < params.questions.length; i++) {\n\t\tconst a = result.answers.find((x) => x.questionIndex === i);\n\t\tif (a) segments.push(buildAnswerSegment(a));\n\t}\n\tif (containsChatAnswer) {\n\t\tconst answerSegments = segments.length > 0 ? ` ${segments.join(\" \")}` : \"\";\n\t\treturn buildToolResult(`${CHAT_TERMINATION_DIRECTIVE}${answerSegments}`, result, { terminate: true });\n\t}\n\tif (segments.length === 0) {\n\t\treturn buildToolResult(DECLINE_MESSAGE, { answers: result.answers, cancelled: true });\n\t}\n\treturn buildToolResult(`${ENVELOPE_PREFIX} ${segments.join(\" \")} ${ENVELOPE_SUFFIX}`, result);\n}\n\n/**\n * Format a single answer segment for the envelope. Pure of `a`. The `\"Q\"=\"A\"` shape and\n * the optional `selected preview:` / `user notes:` suffixes are pinned by envelope tests.\n */\nexport function buildAnswerSegment(a: QuestionAnswer): string {\n\tconst parts: string[] = [`\"${a.question}\"=\"${formatAnswerScalar(a, \"envelope\")}\"`];\n\tif (a.preview && a.preview.length > 0) parts.push(`selected preview: ${a.preview}`);\n\tif (a.notes && a.notes.length > 0) parts.push(`user notes: ${a.notes}`);\n\treturn `${parts.join(\". \")}.`;\n}\n\nexport function buildToolResult(text: string, details: QuestionnaireResult, options?: { terminate?: boolean }) {\n\treturn {\n\t\tcontent: [{ type: \"text\" as const, text }],\n\t\tdetails,\n\t\t...(options?.terminate === true ? { terminate: true } : {}),\n\t};\n}\n"]}
|
|
@@ -2,10 +2,22 @@ import { formatAnswerScalar } from "./format-answer.js";
|
|
|
2
2
|
export const DECLINE_MESSAGE = "User declined to answer questions";
|
|
3
3
|
export const ENVELOPE_PREFIX = "User has answered your questions:";
|
|
4
4
|
export const ENVELOPE_SUFFIX = "You can now continue with the user's answers in mind.";
|
|
5
|
+
const CHAT_TERMINATION_DIRECTIVE = "User wants to chat about this before choosing. Stop the current task flow and wait for the user's next message.";
|
|
6
|
+
/**
|
|
7
|
+
* True when any answer in the result carries `kind: "chat"`.
|
|
8
|
+
* Used by `buildQuestionnaireResponse` to switch to the terminate path.
|
|
9
|
+
*/
|
|
10
|
+
export function hasChatAnswer(result) {
|
|
11
|
+
return result.answers.some((a) => a.kind === "chat");
|
|
12
|
+
}
|
|
5
13
|
/**
|
|
6
14
|
* Map a `QuestionnaireResult` (or null/cancelled) to the LLM-facing tool envelope.
|
|
7
|
-
* Pure of `(result, params)`; cancelled and "no segments" both fall to
|
|
8
|
-
* so the model sees a single canonical "didn't answer" signal
|
|
15
|
+
* Pure of `(result, params)`; cancelled and non-chat "no segments" both fall to
|
|
16
|
+
* `DECLINE_MESSAGE` so the model sees a single canonical "didn't answer" signal
|
|
17
|
+
* regardless of why.
|
|
18
|
+
*
|
|
19
|
+
* Chat rule: when any non-cancelled answer is `kind: "chat"`, the result carries
|
|
20
|
+
* `terminate: true` and stop/wait wording instead of the generic continuation suffix.
|
|
9
21
|
*/
|
|
10
22
|
export function buildQuestionnaireResponse(result, params) {
|
|
11
23
|
if (!result || result.cancelled) {
|
|
@@ -14,12 +26,17 @@ export function buildQuestionnaireResponse(result, params) {
|
|
|
14
26
|
cancelled: true,
|
|
15
27
|
});
|
|
16
28
|
}
|
|
29
|
+
const containsChatAnswer = hasChatAnswer(result);
|
|
17
30
|
const segments = [];
|
|
18
31
|
for (let i = 0; i < params.questions.length; i++) {
|
|
19
32
|
const a = result.answers.find((x) => x.questionIndex === i);
|
|
20
33
|
if (a)
|
|
21
34
|
segments.push(buildAnswerSegment(a));
|
|
22
35
|
}
|
|
36
|
+
if (containsChatAnswer) {
|
|
37
|
+
const answerSegments = segments.length > 0 ? ` ${segments.join(" ")}` : "";
|
|
38
|
+
return buildToolResult(`${CHAT_TERMINATION_DIRECTIVE}${answerSegments}`, result, { terminate: true });
|
|
39
|
+
}
|
|
23
40
|
if (segments.length === 0) {
|
|
24
41
|
return buildToolResult(DECLINE_MESSAGE, { answers: result.answers, cancelled: true });
|
|
25
42
|
}
|
|
@@ -37,10 +54,11 @@ export function buildAnswerSegment(a) {
|
|
|
37
54
|
parts.push(`user notes: ${a.notes}`);
|
|
38
55
|
return `${parts.join(". ")}.`;
|
|
39
56
|
}
|
|
40
|
-
export function buildToolResult(text, details) {
|
|
57
|
+
export function buildToolResult(text, details, options) {
|
|
41
58
|
return {
|
|
42
59
|
content: [{ type: "text", text }],
|
|
43
60
|
details,
|
|
61
|
+
...(options?.terminate === true ? { terminate: true } : {}),
|
|
44
62
|
};
|
|
45
63
|
}
|
|
46
64
|
//# sourceMappingURL=response-envelope.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"response-envelope.js","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/tool/response-envelope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAGxD,MAAM,CAAC,MAAM,eAAe,GAAG,mCAAmC,CAAC;AACnE,MAAM,CAAC,MAAM,eAAe,GAAG,mCAAmC,CAAC;AACnE,MAAM,CAAC,MAAM,eAAe,GAAG,uDAAuD,CAAC;
|
|
1
|
+
{"version":3,"file":"response-envelope.js","sourceRoot":"","sources":["../../../../../src/core/tools/ask-user-question/tool/response-envelope.ts"],"names":[],"mappings":"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","sourcesContent":["import { formatAnswerScalar } from \"./format-answer.ts\";\nimport type { QuestionAnswer, QuestionnaireResult, QuestionParams } from \"./types.ts\";\n\nexport const DECLINE_MESSAGE = \"User declined to answer questions\";\nexport const ENVELOPE_PREFIX = \"User has answered your questions:\";\nexport const ENVELOPE_SUFFIX = \"You can now continue with the user's answers in mind.\";\nconst CHAT_TERMINATION_DIRECTIVE =\n\t\"User wants to chat about this before choosing. Stop the current task flow and wait for the user's next message.\";\n\n/**\n * True when any answer in the result carries `kind: \"chat\"`.\n * Used by `buildQuestionnaireResponse` to switch to the terminate path.\n */\nexport function hasChatAnswer(result: QuestionnaireResult): boolean {\n\treturn result.answers.some((a) => a.kind === \"chat\");\n}\n\n/**\n * Map a `QuestionnaireResult` (or null/cancelled) to the LLM-facing tool envelope.\n * Pure of `(result, params)`; cancelled and non-chat \"no segments\" both fall to\n * `DECLINE_MESSAGE` so the model sees a single canonical \"didn't answer\" signal\n * regardless of why.\n *\n * Chat rule: when any non-cancelled answer is `kind: \"chat\"`, the result carries\n * `terminate: true` and stop/wait wording instead of the generic continuation suffix.\n */\nexport function buildQuestionnaireResponse(result: QuestionnaireResult | null | undefined, params: QuestionParams) {\n\tif (!result || result.cancelled) {\n\t\treturn buildToolResult(DECLINE_MESSAGE, {\n\t\t\tanswers: result?.answers ?? [],\n\t\t\tcancelled: true,\n\t\t});\n\t}\n\tconst containsChatAnswer = hasChatAnswer(result);\n\tconst segments: string[] = [];\n\tfor (let i = 0; i < params.questions.length; i++) {\n\t\tconst a = result.answers.find((x) => x.questionIndex === i);\n\t\tif (a) segments.push(buildAnswerSegment(a));\n\t}\n\tif (containsChatAnswer) {\n\t\tconst answerSegments = segments.length > 0 ? ` ${segments.join(\" \")}` : \"\";\n\t\treturn buildToolResult(`${CHAT_TERMINATION_DIRECTIVE}${answerSegments}`, result, { terminate: true });\n\t}\n\tif (segments.length === 0) {\n\t\treturn buildToolResult(DECLINE_MESSAGE, { answers: result.answers, cancelled: true });\n\t}\n\treturn buildToolResult(`${ENVELOPE_PREFIX} ${segments.join(\" \")} ${ENVELOPE_SUFFIX}`, result);\n}\n\n/**\n * Format a single answer segment for the envelope. Pure of `a`. The `\"Q\"=\"A\"` shape and\n * the optional `selected preview:` / `user notes:` suffixes are pinned by envelope tests.\n */\nexport function buildAnswerSegment(a: QuestionAnswer): string {\n\tconst parts: string[] = [`\"${a.question}\"=\"${formatAnswerScalar(a, \"envelope\")}\"`];\n\tif (a.preview && a.preview.length > 0) parts.push(`selected preview: ${a.preview}`);\n\tif (a.notes && a.notes.length > 0) parts.push(`user notes: ${a.notes}`);\n\treturn `${parts.join(\". \")}.`;\n}\n\nexport function buildToolResult(text: string, details: QuestionnaireResult, options?: { terminate?: boolean }) {\n\treturn {\n\t\tcontent: [{ type: \"text\" as const, text }],\n\t\tdetails,\n\t\t...(options?.terminate === true ? { terminate: true } : {}),\n\t};\n}\n"]}
|