smithers-orchestrator 0.8.1 → 0.8.2
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/package.json +1 -1
- package/src/SmithersCtx.ts +37 -1
- package/src/TaskDescriptor.ts +2 -3
- package/src/agents/AmpAgent.ts +94 -0
- package/src/agents/BaseCliAgent.ts +2 -0
- package/src/agents/GeminiAgent.ts +10 -2
- package/src/agents/KimiAgent.ts +11 -3
- package/src/agents/index.ts +2 -0
- package/src/components/Task.ts +2 -3
- package/src/dom/extract.ts +0 -2
- package/src/engine/index.ts +47 -15
- package/src/index.ts +1 -0
package/package.json
CHANGED
package/src/SmithersCtx.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import type { OutputKey } from "./OutputKey";
|
|
2
2
|
import type { OutputAccessor, InferOutputEntry } from "./OutputAccessor";
|
|
3
|
+
import type { z } from "zod";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reverse-lookup: given Schema and a value type V, find the key K where Schema[K] extends V.
|
|
7
|
+
* Used to narrow return types when passing Zod schema objects directly.
|
|
8
|
+
*/
|
|
9
|
+
type SchemaKeyForValue<Schema, V> = {
|
|
10
|
+
[K in keyof Schema & string]: Schema[K] extends V ? K : never;
|
|
11
|
+
}[keyof Schema & string];
|
|
3
12
|
|
|
4
13
|
export interface SmithersCtx<Schema> {
|
|
5
14
|
runId: string;
|
|
@@ -8,22 +17,49 @@ export interface SmithersCtx<Schema> {
|
|
|
8
17
|
input: Schema extends { input: infer T } ? T : Record<string, unknown>;
|
|
9
18
|
outputs: OutputAccessor<Schema>;
|
|
10
19
|
|
|
20
|
+
// Overload: pass Zod schema value directly → narrowed return type
|
|
21
|
+
output<V extends z.ZodTypeAny>(
|
|
22
|
+
table: V,
|
|
23
|
+
key: OutputKey,
|
|
24
|
+
): SchemaKeyForValue<Schema, V> extends never
|
|
25
|
+
? InferOutputEntry<V>
|
|
26
|
+
: InferOutputEntry<V>;
|
|
27
|
+
|
|
28
|
+
// Overload: pass string key → narrowed via K
|
|
11
29
|
output<K extends keyof Schema & string>(
|
|
12
30
|
table: K,
|
|
13
31
|
key: OutputKey,
|
|
14
32
|
): InferOutputEntry<Schema[K]>;
|
|
15
33
|
|
|
34
|
+
// Overload: pass Zod schema value directly → narrowed return type
|
|
35
|
+
outputMaybe<V extends z.ZodTypeAny>(
|
|
36
|
+
table: V,
|
|
37
|
+
key: OutputKey,
|
|
38
|
+
): SchemaKeyForValue<Schema, V> extends never
|
|
39
|
+
? InferOutputEntry<V> | undefined
|
|
40
|
+
: InferOutputEntry<V> | undefined;
|
|
41
|
+
|
|
42
|
+
// Overload: pass string key → narrowed via K
|
|
16
43
|
outputMaybe<K extends keyof Schema & string>(
|
|
17
44
|
table: K,
|
|
18
45
|
key: OutputKey,
|
|
19
46
|
): InferOutputEntry<Schema[K]> | undefined;
|
|
20
47
|
|
|
48
|
+
// Overload: pass Zod schema value directly → narrowed return type
|
|
49
|
+
latest<V extends z.ZodTypeAny>(
|
|
50
|
+
table: V,
|
|
51
|
+
nodeId: string,
|
|
52
|
+
): SchemaKeyForValue<Schema, V> extends never
|
|
53
|
+
? InferOutputEntry<V> | undefined
|
|
54
|
+
: InferOutputEntry<V> | undefined;
|
|
55
|
+
|
|
56
|
+
// Overload: pass string key → narrowed via K
|
|
21
57
|
latest<K extends keyof Schema & string>(
|
|
22
58
|
table: K,
|
|
23
59
|
nodeId: string,
|
|
24
60
|
): InferOutputEntry<Schema[K]> | undefined;
|
|
25
61
|
|
|
26
|
-
latestArray(value: unknown, schema:
|
|
62
|
+
latestArray(value: unknown, schema: z.ZodType): any[];
|
|
27
63
|
|
|
28
64
|
iterationCount(table: any, nodeId: string): number;
|
|
29
65
|
}
|
package/src/TaskDescriptor.ts
CHANGED
|
@@ -17,9 +17,8 @@ export type TaskDescriptor = {
|
|
|
17
17
|
retries: number;
|
|
18
18
|
timeoutMs: number | null;
|
|
19
19
|
continueOnFail: boolean;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
fallbackAgent?: AgentLike;
|
|
20
|
+
/** Agent or array of agents [primary, fallback1, fallback2, ...]. Tries in order until one succeeds. */
|
|
21
|
+
agent?: AgentLike | AgentLike[];
|
|
23
22
|
prompt?: string;
|
|
24
23
|
staticPayload?: unknown;
|
|
25
24
|
computeFn?: () => unknown | Promise<unknown>;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseCliAgent,
|
|
3
|
+
pushFlag,
|
|
4
|
+
} from "./BaseCliAgent";
|
|
5
|
+
import type { BaseCliAgentOptions } from "./BaseCliAgent";
|
|
6
|
+
|
|
7
|
+
type AmpAgentOptions = BaseCliAgentOptions & {
|
|
8
|
+
workDir?: string;
|
|
9
|
+
thread?: string;
|
|
10
|
+
visibility?: "private" | "public" | "workspace" | "group";
|
|
11
|
+
quiet?: boolean;
|
|
12
|
+
mcpConfig?: string;
|
|
13
|
+
settingsFile?: string;
|
|
14
|
+
logLevel?: "error" | "warn" | "info" | "debug" | "audit";
|
|
15
|
+
logFile?: string;
|
|
16
|
+
dangerouslyAllowAll?: boolean;
|
|
17
|
+
ide?: boolean;
|
|
18
|
+
jetbrains?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class AmpAgent extends BaseCliAgent {
|
|
22
|
+
private readonly opts: AmpAgentOptions;
|
|
23
|
+
|
|
24
|
+
constructor(opts: AmpAgentOptions = {}) {
|
|
25
|
+
super(opts);
|
|
26
|
+
this.opts = opts;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected async buildCommand(params: {
|
|
30
|
+
prompt: string;
|
|
31
|
+
systemPrompt?: string;
|
|
32
|
+
cwd: string;
|
|
33
|
+
options: any;
|
|
34
|
+
}) {
|
|
35
|
+
const args: string[] = ["threads", "continue"];
|
|
36
|
+
const yoloEnabled = this.opts.yolo ?? this.yolo;
|
|
37
|
+
|
|
38
|
+
// Working directory
|
|
39
|
+
pushFlag(args, "--work-dir", this.opts.workDir ?? params.cwd);
|
|
40
|
+
|
|
41
|
+
// Thread ID (if continuing existing thread)
|
|
42
|
+
pushFlag(args, "--thread", this.opts.thread);
|
|
43
|
+
|
|
44
|
+
// Visibility for new threads
|
|
45
|
+
pushFlag(args, "--visibility", this.opts.visibility);
|
|
46
|
+
|
|
47
|
+
// Model
|
|
48
|
+
pushFlag(args, "--model", this.opts.model ?? this.model);
|
|
49
|
+
|
|
50
|
+
// Quiet mode
|
|
51
|
+
if (this.opts.quiet) args.push("--quiet");
|
|
52
|
+
|
|
53
|
+
// MCP config
|
|
54
|
+
pushFlag(args, "--mcp-config", this.opts.mcpConfig);
|
|
55
|
+
|
|
56
|
+
// Settings file
|
|
57
|
+
pushFlag(args, "--settings-file", this.opts.settingsFile);
|
|
58
|
+
|
|
59
|
+
// Log level
|
|
60
|
+
pushFlag(args, "--log-level", this.opts.logLevel);
|
|
61
|
+
|
|
62
|
+
// Log file
|
|
63
|
+
pushFlag(args, "--log-file", this.opts.logFile);
|
|
64
|
+
|
|
65
|
+
// Dangerous allow all (yolo mode)
|
|
66
|
+
if (this.opts.dangerouslyAllowAll || yoloEnabled) {
|
|
67
|
+
args.push("--dangerously-allow-all");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// IDE integration
|
|
71
|
+
if (this.opts.ide === false) args.push("--no-ide");
|
|
72
|
+
if (this.opts.jetbrains === false) args.push("--no-jetbrains");
|
|
73
|
+
|
|
74
|
+
// Color handling
|
|
75
|
+
args.push("--no-color"); // Disable color for clean output parsing
|
|
76
|
+
|
|
77
|
+
if (this.extraArgs?.length) args.push(...this.extraArgs);
|
|
78
|
+
|
|
79
|
+
// Build prompt with system prompt prepended
|
|
80
|
+
const systemPrefix = params.systemPrompt
|
|
81
|
+
? `${params.systemPrompt}\n\n`
|
|
82
|
+
: "";
|
|
83
|
+
const fullPrompt = `${systemPrefix}${params.prompt ?? ""}`;
|
|
84
|
+
|
|
85
|
+
// Amp accepts prompt as final argument
|
|
86
|
+
args.push(fullPrompt);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
command: "amp",
|
|
90
|
+
args,
|
|
91
|
+
outputFormat: "text" as const,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -190,6 +190,7 @@ export function extractTextFromJsonValue(value: any): string | undefined {
|
|
|
190
190
|
.join("");
|
|
191
191
|
if (parts.trim()) return parts;
|
|
192
192
|
}
|
|
193
|
+
if (value.response) return extractTextFromJsonValue(value.response);
|
|
193
194
|
if (value.message) return extractTextFromJsonValue(value.message);
|
|
194
195
|
if (value.result) return extractTextFromJsonValue(value.result);
|
|
195
196
|
if (value.output) return extractTextFromJsonValue(value.output);
|
|
@@ -780,6 +781,7 @@ export abstract class BaseCliAgent implements Agent<any, any, any> {
|
|
|
780
781
|
outputFormat === "json" || outputFormat === "stream-json"
|
|
781
782
|
? (extractTextFromJsonPayload(rawText) ?? rawText)
|
|
782
783
|
: rawText;
|
|
784
|
+
|
|
783
785
|
const output = tryParseJson(extractedText);
|
|
784
786
|
return buildGenerateResult(
|
|
785
787
|
extractedText,
|
|
@@ -40,7 +40,10 @@ export class GeminiAgent extends BaseCliAgent {
|
|
|
40
40
|
}) {
|
|
41
41
|
const args: string[] = [];
|
|
42
42
|
const yoloEnabled = this.opts.yolo ?? this.yolo;
|
|
43
|
-
|
|
43
|
+
// Default to "json" output format to separate model responses from tool
|
|
44
|
+
// output text. With "text" format, tool call results (file contents etc.)
|
|
45
|
+
// are concatenated into the response, making JSON extraction unreliable.
|
|
46
|
+
const outputFormat = this.opts.outputFormat ?? "json";
|
|
44
47
|
|
|
45
48
|
if (this.opts.debug) args.push("--debug");
|
|
46
49
|
pushFlag(args, "--model", this.opts.model ?? this.model);
|
|
@@ -70,7 +73,12 @@ export class GeminiAgent extends BaseCliAgent {
|
|
|
70
73
|
const systemPrefix = params.systemPrompt
|
|
71
74
|
? `${params.systemPrompt}\n\n`
|
|
72
75
|
: "";
|
|
73
|
-
|
|
76
|
+
// Reinforce JSON output requirement in the prompt for Gemini models which
|
|
77
|
+
// tend to forget structured output instructions on long responses.
|
|
78
|
+
const jsonReminder = params.prompt?.includes("REQUIRED OUTPUT")
|
|
79
|
+
? "\n\nREMINDER: Your response MUST end with a ```json code fence containing the required JSON object. Do NOT skip this step — the pipeline will reject your response without it.\n"
|
|
80
|
+
: "";
|
|
81
|
+
const fullPrompt = `${systemPrefix}${params.prompt ?? ""}${jsonReminder}`;
|
|
74
82
|
args.push("--prompt", fullPrompt);
|
|
75
83
|
|
|
76
84
|
return {
|
package/src/agents/KimiAgent.ts
CHANGED
|
@@ -46,9 +46,14 @@ export class KimiAgent extends BaseCliAgent {
|
|
|
46
46
|
// Note: --print implicitly adds --yolo
|
|
47
47
|
args.push("--print");
|
|
48
48
|
|
|
49
|
-
// Output format
|
|
50
|
-
|
|
49
|
+
// Output format — use text with --final-message-only to get only the
|
|
50
|
+
// model's final response without tool call outputs mixed in.
|
|
51
|
+
const outputFormat = this.opts.outputFormat ?? "text";
|
|
51
52
|
pushFlag(args, "--output-format", outputFormat);
|
|
53
|
+
// When using text format, --final-message-only ensures we only get
|
|
54
|
+
// the model's final response, not intermediate tool output.
|
|
55
|
+
const finalMessageOnly = this.opts.finalMessageOnly ?? (outputFormat === "text");
|
|
56
|
+
if (finalMessageOnly) args.push("--final-message-only");
|
|
52
57
|
|
|
53
58
|
// Other flags
|
|
54
59
|
pushFlag(args, "--work-dir", this.opts.workDir ?? params.cwd);
|
|
@@ -76,7 +81,10 @@ export class KimiAgent extends BaseCliAgent {
|
|
|
76
81
|
const systemPrefix = params.systemPrompt
|
|
77
82
|
? `${params.systemPrompt}\n\n`
|
|
78
83
|
: "";
|
|
79
|
-
const
|
|
84
|
+
const jsonReminder = params.prompt?.includes("REQUIRED OUTPUT")
|
|
85
|
+
? "\n\nREMINDER: Your response MUST end with a ```json code fence containing the required JSON object. Do NOT skip this step — the pipeline will reject your response without it.\n"
|
|
86
|
+
: "";
|
|
87
|
+
const fullPrompt = `${systemPrefix}${params.prompt ?? ""}${jsonReminder}`;
|
|
80
88
|
|
|
81
89
|
// Pass prompt via --prompt flag
|
|
82
90
|
pushFlag(args, "--prompt", fullPrompt);
|
package/src/agents/index.ts
CHANGED
package/src/components/Task.ts
CHANGED
|
@@ -8,9 +8,8 @@ export type TaskProps<Row> = {
|
|
|
8
8
|
key?: string;
|
|
9
9
|
id: string;
|
|
10
10
|
output: import("zod").ZodObject<any>;
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
fallbackAgent?: AgentLike;
|
|
11
|
+
/** Agent or array of agents [primary, fallback1, fallback2, ...]. Tries in order on retries. */
|
|
12
|
+
agent?: AgentLike | AgentLike[];
|
|
14
13
|
skipIf?: boolean;
|
|
15
14
|
needsApproval?: boolean;
|
|
16
15
|
timeoutMs?: number;
|
package/src/dom/extract.ts
CHANGED
|
@@ -208,7 +208,6 @@ export function extractFromHost(
|
|
|
208
208
|
const continueOnFail = Boolean(raw.continueOnFail);
|
|
209
209
|
|
|
210
210
|
const agent = raw.agent;
|
|
211
|
-
const fallbackAgent = raw.fallbackAgent;
|
|
212
211
|
const kind = raw.__smithersKind;
|
|
213
212
|
const isAgent = kind === "agent" || Boolean(agent);
|
|
214
213
|
const prompt = isAgent ? String(raw.children ?? "") : undefined;
|
|
@@ -243,7 +242,6 @@ export function extractFromHost(
|
|
|
243
242
|
timeoutMs,
|
|
244
243
|
continueOnFail,
|
|
245
244
|
agent,
|
|
246
|
-
fallbackAgent,
|
|
247
245
|
prompt,
|
|
248
246
|
staticPayload,
|
|
249
247
|
computeFn,
|
package/src/engine/index.ts
CHANGED
|
@@ -778,9 +778,9 @@ async function executeTask(
|
|
|
778
778
|
}
|
|
779
779
|
|
|
780
780
|
if (!payload) {
|
|
781
|
-
const
|
|
782
|
-
|
|
783
|
-
if (
|
|
781
|
+
const agents = Array.isArray(desc.agent) ? desc.agent : (desc.agent ? [desc.agent] : []);
|
|
782
|
+
const effectiveAgent = agents[Math.min(attemptNo - 1, agents.length - 1)];
|
|
783
|
+
if (effectiveAgent) {
|
|
784
784
|
// Use fallback agent on retry attempts when available
|
|
785
785
|
const result = await runWithToolContext(
|
|
786
786
|
{
|
|
@@ -802,15 +802,23 @@ async function executeTask(
|
|
|
802
802
|
let effectivePrompt = desc.prompt ?? "";
|
|
803
803
|
if (desc.outputTable) {
|
|
804
804
|
const schemaDesc = describeSchemaShape(desc.outputTable as any, desc.outputSchema);
|
|
805
|
-
|
|
806
|
-
"",
|
|
807
|
-
"",
|
|
805
|
+
const jsonInstructions = [
|
|
808
806
|
"**REQUIRED OUTPUT** — You MUST end your response with a JSON object in a code fence matching this schema:",
|
|
809
807
|
"```json",
|
|
810
808
|
schemaDesc,
|
|
811
809
|
"```",
|
|
812
810
|
"Output the JSON at the END of your response. The workflow will fail without it.",
|
|
813
811
|
].join("\n");
|
|
812
|
+
// Prepend a brief reminder at the top AND append full instructions at the end.
|
|
813
|
+
// This ensures models with long outputs don't lose track of the JSON requirement.
|
|
814
|
+
effectivePrompt = [
|
|
815
|
+
"IMPORTANT: After completing the task below, you MUST output a JSON object in a ```json code fence at the very end of your response. Do NOT forget this — the workflow fails without it.",
|
|
816
|
+
"",
|
|
817
|
+
effectivePrompt,
|
|
818
|
+
"",
|
|
819
|
+
"",
|
|
820
|
+
jsonInstructions,
|
|
821
|
+
].join("\n");
|
|
814
822
|
}
|
|
815
823
|
const emitOutput = (text: string, stream: "stdout" | "stderr") => {
|
|
816
824
|
eventBus.emit("event", {
|
|
@@ -869,7 +877,7 @@ async function executeTask(
|
|
|
869
877
|
// Not valid JSON, try extraction
|
|
870
878
|
}
|
|
871
879
|
|
|
872
|
-
// Helper to extract balanced JSON from text
|
|
880
|
+
// Helper to extract balanced JSON from text (first occurrence)
|
|
873
881
|
function extractBalancedJson(str: string): string | null {
|
|
874
882
|
const start = str.indexOf("{");
|
|
875
883
|
if (start === -1) return null;
|
|
@@ -902,13 +910,27 @@ async function executeTask(
|
|
|
902
910
|
return null;
|
|
903
911
|
}
|
|
904
912
|
|
|
913
|
+
// Helper to extract the LAST balanced JSON object in text.
|
|
914
|
+
// Agents like Kimi emit all intermediate tool output before the final
|
|
915
|
+
// required JSON, so searching from the end finds the right object.
|
|
916
|
+
function extractLastBalancedJson(str: string): string | null {
|
|
917
|
+
let pos = str.lastIndexOf("{");
|
|
918
|
+
while (pos >= 0) {
|
|
919
|
+
const json = extractBalancedJson(str.slice(pos));
|
|
920
|
+
if (json !== null) return json;
|
|
921
|
+
pos = str.lastIndexOf("{", pos - 1);
|
|
922
|
+
}
|
|
923
|
+
return null;
|
|
924
|
+
}
|
|
925
|
+
|
|
905
926
|
// Try to extract JSON from code fence (```json ... ```)
|
|
906
927
|
if (output === undefined) {
|
|
907
|
-
//
|
|
908
|
-
const
|
|
909
|
-
|
|
928
|
+
// Find the LAST code fence — the required output is always at the end
|
|
929
|
+
const allFences = [...text.matchAll(/```(?:json)?\s*\{/g)];
|
|
930
|
+
const lastFence = allFences[allFences.length - 1];
|
|
931
|
+
if (lastFence?.index !== undefined) {
|
|
910
932
|
const afterFence = text
|
|
911
|
-
.slice(
|
|
933
|
+
.slice(lastFence.index)
|
|
912
934
|
.replace(/```(?:json)?\s*/, "");
|
|
913
935
|
const jsonStr = extractBalancedJson(afterFence);
|
|
914
936
|
if (jsonStr) {
|
|
@@ -965,9 +987,10 @@ async function executeTask(
|
|
|
965
987
|
}
|
|
966
988
|
}
|
|
967
989
|
|
|
968
|
-
// Try text itself
|
|
990
|
+
// Try text itself — search from END so we get the required output JSON,
|
|
991
|
+
// not an earlier JSON object from intermediate tool output
|
|
969
992
|
if (output === undefined) {
|
|
970
|
-
const jsonStr =
|
|
993
|
+
const jsonStr = extractLastBalancedJson(text);
|
|
971
994
|
if (jsonStr) {
|
|
972
995
|
try {
|
|
973
996
|
const parsed = JSON.parse(jsonStr);
|
|
@@ -983,8 +1006,16 @@ async function executeTask(
|
|
|
983
1006
|
// If no JSON found, send a follow-up prompt asking for just the JSON with schema info
|
|
984
1007
|
if (output === undefined && desc.agent) {
|
|
985
1008
|
const schemaDesc = describeSchemaShape(desc.outputTable as any, desc.outputSchema);
|
|
1009
|
+
// Include a truncated summary of the original response so the model has context
|
|
1010
|
+
const responseSummary = text.length > 2000
|
|
1011
|
+
? text.slice(0, 1000) + "\n...[truncated]...\n" + text.slice(-1000)
|
|
1012
|
+
: text;
|
|
986
1013
|
const jsonPrompt = [
|
|
987
|
-
`You
|
|
1014
|
+
`You previously completed a task and produced this response (possibly truncated):`,
|
|
1015
|
+
``,
|
|
1016
|
+
responseSummary,
|
|
1017
|
+
``,
|
|
1018
|
+
`Now you MUST output ONLY a valid JSON object (no other text) summarizing your work above, with exactly these fields and types:`,
|
|
988
1019
|
schemaDesc,
|
|
989
1020
|
``,
|
|
990
1021
|
`Output ONLY the JSON object, nothing else.`,
|
|
@@ -1028,7 +1059,8 @@ async function executeTask(
|
|
|
1028
1059
|
console.log(
|
|
1029
1060
|
`[JSON Debug] finishReason=${finishReason}, text.length=${text.length}, steps.count=${debugSteps.length}`,
|
|
1030
1061
|
);
|
|
1031
|
-
console.log(`[JSON Debug] text
|
|
1062
|
+
console.log(`[JSON Debug] text start: ${text.slice(0, 300)}`);
|
|
1063
|
+
console.log(`[JSON Debug] text end: ${text.slice(-500)}`);
|
|
1032
1064
|
console.log(
|
|
1033
1065
|
`[JSON Debug] last step text: ${debugSteps[debugSteps.length - 1]?.text?.slice(0, 500) ?? "none"}`,
|
|
1034
1066
|
);
|