gaunt-sloth 2.0.0-alpha.2 → 2.0.0-alpha.20
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/README.md +55 -5
- package/cli.js +43 -10
- package/dist/cli.js +33 -2
- package/dist/cli.js.map +1 -1
- package/dist/commands/adkEvalRunner.d.ts +66 -0
- package/dist/commands/adkEvalRunner.js +69 -0
- package/dist/commands/adkEvalRunner.js.map +1 -0
- package/dist/commands/agUiEvalRunner.d.ts +73 -0
- package/dist/commands/agUiEvalRunner.js +216 -0
- package/dist/commands/agUiEvalRunner.js.map +1 -0
- package/dist/commands/askCommand.d.ts +3 -2
- package/dist/commands/askCommand.js +26 -7
- package/dist/commands/askCommand.js.map +1 -1
- package/dist/commands/batchCommand.d.ts +78 -0
- package/dist/commands/batchCommand.js +245 -0
- package/dist/commands/batchCommand.js.map +1 -0
- package/dist/commands/cliOptionParsers.d.ts +12 -0
- package/dist/commands/cliOptionParsers.js +18 -0
- package/dist/commands/cliOptionParsers.js.map +1 -0
- package/dist/commands/commandIntrospection.d.ts +6 -6
- package/dist/commands/commandIntrospection.js +12 -12
- package/dist/commands/commandIntrospection.js.map +1 -1
- package/dist/commands/commandUtils.d.ts +16 -16
- package/dist/commands/commandUtils.js +20 -20
- package/dist/commands/commandUtils.js.map +1 -1
- package/dist/commands/configCommand.d.ts +24 -0
- package/dist/commands/configCommand.js +117 -0
- package/dist/commands/configCommand.js.map +1 -0
- package/dist/commands/configSetup.d.ts +1 -1
- package/dist/commands/configSetup.js +17 -2
- package/dist/commands/configSetup.js.map +1 -1
- package/dist/commands/evalCommand.d.ts +38 -0
- package/dist/commands/evalCommand.js +270 -0
- package/dist/commands/evalCommand.js.map +1 -0
- package/dist/commands/execCommand.js +4 -1
- package/dist/commands/execCommand.js.map +1 -1
- package/dist/commands/firstRunDialog.d.ts +39 -5
- package/dist/commands/firstRunDialog.js +72 -10
- package/dist/commands/firstRunDialog.js.map +1 -1
- package/dist/commands/getCommand.js +7 -7
- package/dist/commands/getCommand.js.map +1 -1
- package/dist/commands/historyCommand.d.ts +17 -0
- package/dist/commands/historyCommand.js +102 -0
- package/dist/commands/historyCommand.js.map +1 -0
- package/dist/commands/initCommand.d.ts +3 -0
- package/dist/commands/initCommand.js +9 -4
- package/dist/commands/initCommand.js.map +1 -1
- package/dist/commands/insightsCommand.d.ts +8 -0
- package/dist/commands/insightsCommand.js +32 -0
- package/dist/commands/insightsCommand.js.map +1 -0
- package/dist/commands/modelsCommand.d.ts +25 -0
- package/dist/commands/modelsCommand.js +96 -0
- package/dist/commands/modelsCommand.js.map +1 -0
- package/dist/commands/prCommand.js +14 -15
- package/dist/commands/prCommand.js.map +1 -1
- package/dist/commands/prDiscovery.d.ts +2 -2
- package/dist/commands/prDiscovery.js +27 -29
- package/dist/commands/prDiscovery.js.map +1 -1
- package/dist/commands/reviewCommand.js +15 -10
- package/dist/commands/reviewCommand.js.map +1 -1
- package/dist/commands/workflowCommand.d.ts +19 -0
- package/dist/commands/workflowCommand.js +77 -0
- package/dist/commands/workflowCommand.js.map +1 -0
- package/dist/helpers/jira/jiraLogWork.js.map +1 -1
- package/dist/modules/startSession.js +9 -7
- package/dist/modules/startSession.js.map +1 -1
- package/dist/tools/gthJiraLogWorkTool.js +3 -3
- package/dist/tools/gthJiraLogWorkTool.js.map +1 -1
- package/dist/tui/components/App.js +321 -21
- package/dist/tui/components/App.js.map +1 -1
- package/dist/tui/components/ApprovalPrompt.d.ts +18 -0
- package/dist/tui/components/ApprovalPrompt.js +26 -0
- package/dist/tui/components/ApprovalPrompt.js.map +1 -0
- package/dist/tui/components/DebugPanel.d.ts +29 -9
- package/dist/tui/components/DebugPanel.js +46 -23
- package/dist/tui/components/DebugPanel.js.map +1 -1
- package/dist/tui/components/FetchProgress.d.ts +36 -0
- package/dist/tui/components/FetchProgress.js +39 -0
- package/dist/tui/components/FetchProgress.js.map +1 -0
- package/dist/tui/components/LiveTurn.d.ts +26 -4
- package/dist/tui/components/LiveTurn.js +60 -8
- package/dist/tui/components/LiveTurn.js.map +1 -1
- package/dist/tui/components/NoticeBar.d.ts +32 -0
- package/dist/tui/components/NoticeBar.js +39 -0
- package/dist/tui/components/NoticeBar.js.map +1 -0
- package/dist/tui/components/PromptInput.d.ts +26 -1
- package/dist/tui/components/PromptInput.js +93 -7
- package/dist/tui/components/PromptInput.js.map +1 -1
- package/dist/tui/components/Rule.js.map +1 -1
- package/dist/tui/components/SelectList.d.ts +98 -9
- package/dist/tui/components/SelectList.js +209 -26
- package/dist/tui/components/SelectList.js.map +1 -1
- package/dist/tui/components/SlashCommandMenu.d.ts +20 -0
- package/dist/tui/components/SlashCommandMenu.js +28 -0
- package/dist/tui/components/SlashCommandMenu.js.map +1 -0
- package/dist/tui/components/StatusBar.d.ts +7 -1
- package/dist/tui/components/StatusBar.js +7 -3
- package/dist/tui/components/StatusBar.js.map +1 -1
- package/dist/tui/components/Transcript.js +6 -1
- package/dist/tui/components/Transcript.js.map +1 -1
- package/dist/tui/debugRender.d.ts +41 -6
- package/dist/tui/debugRender.js +126 -12
- package/dist/tui/debugRender.js.map +1 -1
- package/dist/tui/debugSearch.d.ts +40 -0
- package/dist/tui/debugSearch.js +73 -0
- package/dist/tui/debugSearch.js.map +1 -0
- package/dist/tui/markdown.js +0 -0
- package/dist/tui/markdown.js.map +1 -1
- package/dist/tui/pasteParser.d.ts +21 -0
- package/dist/tui/pasteParser.js +24 -0
- package/dist/tui/pasteParser.js.map +1 -0
- package/dist/tui/selectCancelled.d.ts +13 -0
- package/dist/tui/selectCancelled.js +17 -0
- package/dist/tui/selectCancelled.js.map +1 -0
- package/dist/tui/slashCommands.d.ts +220 -1
- package/dist/tui/slashCommands.js +408 -1
- package/dist/tui/slashCommands.js.map +1 -1
- package/dist/tui/tuiSessionModule.js +242 -23
- package/dist/tui/tuiSessionModule.js.map +1 -1
- package/dist/tui/types.d.ts +96 -7
- package/dist/tui/viewModel.d.ts +20 -0
- package/dist/tui/viewModel.js +41 -0
- package/dist/tui/viewModel.js.map +1 -1
- package/dist/utils/stdinPolicy.d.ts +31 -0
- package/dist/utils/stdinPolicy.js +37 -0
- package/dist/utils/stdinPolicy.js.map +1 -0
- package/package.json +20 -17
package/README.md
CHANGED
|
@@ -39,11 +39,61 @@ This is the only package in the workspace that pulls in AI vendor dependencies d
|
|
|
39
39
|
npm install -g gaunt-sloth
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
-
For full usage documentation see the [root README](
|
|
42
|
+
For full usage documentation see the [root README](https://github.com/pukeko-robotics/gaunt-sloth/blob/main/README.md) and [docs/COMMANDS.md](https://github.com/pukeko-robotics/gaunt-sloth/blob/main/docs/COMMANDS.md).
|
|
43
|
+
|
|
44
|
+
## ACP server (editor integration)
|
|
45
|
+
|
|
46
|
+
Gaunt Sloth can run as an [Agent Client Protocol](https://agentclientprotocol.com/) (ACP)
|
|
47
|
+
server, so an ACP host — Zed, JetBrains, a future Pukeko client — can spawn it as a coding
|
|
48
|
+
agent. It speaks ACP JSON-RPC over **stdio** (no port, no flags beyond the switch below); the
|
|
49
|
+
host launches it as a subprocess.
|
|
50
|
+
|
|
51
|
+
Run it through this package:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
npm install -g gaunt-sloth@alpha
|
|
55
|
+
gaunt-sloth --acp-agent
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Install the app (`gaunt-sloth`), not `@gaunt-sloth/agent`.** The agent package also ships a
|
|
59
|
+
standalone `gaunt-sloth-acp` binary, but the LLM providers (`@langchain/anthropic`, `openai`,
|
|
60
|
+
`google`, …) are `peerDependencies` of `@gaunt-sloth/core` that **only this app package
|
|
61
|
+
declares as real dependencies**. A bare `@gaunt-sloth/agent` install leaves those peers unmet,
|
|
62
|
+
so its ACP server has no provider to build a model from. `gaunt-sloth --acp-agent` runs the
|
|
63
|
+
exact same ACP server but resolves providers out of the app's dependency tree, so every
|
|
64
|
+
configured provider works. (`stdout` is the protocol channel and is kept clean — gsloth's
|
|
65
|
+
status/config output is redirected to `stderr`.)
|
|
66
|
+
|
|
67
|
+
Provider credentials and model selection come from your usual gsloth config
|
|
68
|
+
(`.gsloth.config.*` / env vars such as `ANTHROPIC_API_KEY`); the ACP server reads them via the
|
|
69
|
+
same `initConfig` path as the CLI. Run the host from your project directory (or set its `cwd`)
|
|
70
|
+
so config and the per-session workspace resolve correctly.
|
|
71
|
+
|
|
72
|
+
### Zed
|
|
73
|
+
|
|
74
|
+
Add to Zed `settings.json` (or Settings -> External Agents -> Add Agent -> Custom Agent):
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"agent_servers": {
|
|
79
|
+
"Gaunt Sloth": {
|
|
80
|
+
"type": "custom",
|
|
81
|
+
"command": "gaunt-sloth",
|
|
82
|
+
"args": ["--acp-agent"],
|
|
83
|
+
"env": {}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Point `command` at the global `gaunt-sloth` binary (after `npm install -g gaunt-sloth@alpha`),
|
|
90
|
+
or at an absolute path to `cli.js` for a local build. Other ACP hosts (JetBrains, etc.) take
|
|
91
|
+
the same `command` + `args` pair.
|
|
43
92
|
|
|
44
93
|
## Related packages
|
|
45
94
|
|
|
46
|
-
- [`@gaunt-sloth/core`](
|
|
47
|
-
- [`@gaunt-sloth/agent`](
|
|
48
|
-
- [`@gaunt-sloth/review`](
|
|
49
|
-
- [
|
|
95
|
+
- [`@gaunt-sloth/core`](https://www.npmjs.com/package/@gaunt-sloth/core) — Core utilities, config, and agent infrastructure ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/core))
|
|
96
|
+
- [`@gaunt-sloth/agent`](https://www.npmjs.com/package/@gaunt-sloth/agent) — Agent runtime, built-in tools, API/AG-UI/ACP server ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/agent))
|
|
97
|
+
- [`@gaunt-sloth/review`](https://www.npmjs.com/package/@gaunt-sloth/review) — Review and Q&A modules with standalone CLI ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/review))
|
|
98
|
+
- [`@gaunt-sloth/batch`](https://www.npmjs.com/package/@gaunt-sloth/batch) — Batch / eval / workflow runtime ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/batch))
|
|
99
|
+
- [`gaunt-sloth`](https://www.npmjs.com/package/gaunt-sloth) — Main CLI application (this package) ([source](https://github.com/pukeko-robotics/gaunt-sloth/tree/main/packages/app))
|
package/cli.js
CHANGED
|
@@ -9,15 +9,48 @@ process.on('warning', (warning) => {
|
|
|
9
9
|
console.warn(warning);
|
|
10
10
|
});
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
12
|
+
// --- ACP server bypass -----------------------------------------------------
|
|
13
|
+
// `gaunt-sloth --acp-agent` runs the Agent Client Protocol (ACP) server instead
|
|
14
|
+
// of the normal CLI, so an ACP host (Zed, JetBrains, a future Pukeko client) can
|
|
15
|
+
// spawn the fat `gaunt-sloth` package as a coding-agent subprocess.
|
|
16
|
+
//
|
|
17
|
+
// Why route ACP through the app rather than the standalone `gaunt-sloth-acp`
|
|
18
|
+
// binary in @gaunt-sloth/agent: the LLM providers (@langchain/anthropic, openai,
|
|
19
|
+
// google, …) are peerDependencies of @gaunt-sloth/core, and only this app package
|
|
20
|
+
// declares them as real dependencies. A bare `@gaunt-sloth/agent` install leaves
|
|
21
|
+
// those peers unmet, so its ACP server has no providers to construct a model from.
|
|
22
|
+
// Starting the same ACP server from here resolves providers out of the app's tree.
|
|
23
|
+
//
|
|
24
|
+
// ACP speaks JSON-RPC over stdio, so stdout MUST stay a clean protocol channel:
|
|
25
|
+
// redirect console.log/info (used by initConfig/status output) to stderr BEFORE
|
|
26
|
+
// touching config, exactly as the standalone `gaunt-sloth-acp` entry does.
|
|
27
|
+
if (process.argv.includes('--acp-agent')) {
|
|
28
|
+
for (const method of ['log', 'info']) {
|
|
29
|
+
console[method] = (...args) => process.stderr.write(args.join(' ') + '\n');
|
|
30
|
+
}
|
|
31
|
+
const [{ initConfig }, { displayError }, { startAcpServer }] = await Promise.all([
|
|
32
|
+
import('@gaunt-sloth/core/config.js'),
|
|
33
|
+
import('@gaunt-sloth/core/utils/consoleUtils.js'),
|
|
34
|
+
import('@gaunt-sloth/agent'),
|
|
35
|
+
]);
|
|
36
|
+
try {
|
|
37
|
+
const config = await initConfig({});
|
|
38
|
+
await startAcpServer(config);
|
|
39
|
+
} catch (err) {
|
|
40
|
+
displayError(`ACP server failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
// This is a minimalistic entry point that sets the installDir in systemUtils
|
|
45
|
+
// and delegates to the compiled TypeScript code in dist/cli.js
|
|
46
|
+
const { setEntryPoint } = await import('./dist/utils/systemUtils.js');
|
|
15
47
|
|
|
16
|
-
// Set the installation directory in systemUtils
|
|
17
|
-
setEntryPoint(import.meta.url);
|
|
48
|
+
// Set the installation directory in systemUtils
|
|
49
|
+
setEntryPoint(import.meta.url);
|
|
18
50
|
|
|
19
|
-
// Import and run the compiled TypeScript code
|
|
20
|
-
import('./dist/cli.js').catch((err) => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
});
|
|
51
|
+
// Import and run the compiled TypeScript code
|
|
52
|
+
import('./dist/cli.js').catch((err) => {
|
|
53
|
+
console.error('Failed to load application:', err);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
56
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Command, Option } from 'commander';
|
|
2
2
|
import { askCommand } from '#src/commands/askCommand.js';
|
|
3
3
|
import { execCommand } from '#src/commands/execCommand.js';
|
|
4
|
+
import { batchCommand } from '#src/commands/batchCommand.js';
|
|
5
|
+
import { evalCommand } from '#src/commands/evalCommand.js';
|
|
6
|
+
import { workflowCommand } from '#src/commands/workflowCommand.js';
|
|
4
7
|
import { initCommand } from '#src/commands/initCommand.js';
|
|
5
8
|
import { reviewCommand } from '#src/commands/reviewCommand.js';
|
|
6
9
|
import { prCommand } from '#src/commands/prCommand.js';
|
|
@@ -8,12 +11,17 @@ import { chatCommand } from '#src/commands/chatCommand.js';
|
|
|
8
11
|
import { codeCommand } from '#src/commands/codeCommand.js';
|
|
9
12
|
import { apiCommand } from '#src/commands/apiCommand.js';
|
|
10
13
|
import { getCommand } from '#src/commands/getCommand.js';
|
|
14
|
+
import { configCommand } from '#src/commands/configCommand.js';
|
|
15
|
+
import { historyCommand } from '#src/commands/historyCommand.js';
|
|
16
|
+
import { insightsCommand } from '#src/commands/insightsCommand.js';
|
|
17
|
+
import { modelsCommand } from '#src/commands/modelsCommand.js';
|
|
11
18
|
import { argv, getSlothVersion, readStdin } from '@gaunt-sloth/core/utils/systemUtils.js';
|
|
19
|
+
import { commandSkipsStdin, resolveInvokedCommandName } from '#src/utils/stdinPolicy.js';
|
|
12
20
|
import { coerceBooleanOrString } from '@gaunt-sloth/core/utils/consoleUtils.js';
|
|
13
21
|
const program = new Command();
|
|
14
22
|
program
|
|
15
23
|
.name('gsloth')
|
|
16
|
-
.description('Gaunt Sloth
|
|
24
|
+
.description('Gaunt Sloth reviewing your PRs')
|
|
17
25
|
.version(getSlothVersion())
|
|
18
26
|
.option('--verbose', 'Set LangChain/LangGraph to verbose mode, ' +
|
|
19
27
|
'causing LangChain/LangGraph to log many details to the console. ' +
|
|
@@ -23,7 +31,8 @@ program
|
|
|
23
31
|
.option('-w, --write-output-to-file <value>', 'Write output to file. Accepts true/false or a filename. Shortcuts: -wn or -w0 for false.')
|
|
24
32
|
.option('--tui', 'Force the interactive Ink TUI for chat/code sessions (overrides CI auto-off)')
|
|
25
33
|
.option('--no-tui', 'Force the plain readline session for chat/code (disable the TUI)')
|
|
26
|
-
.addOption(new Option('--nopipe').hideHelp(true))
|
|
34
|
+
.addOption(new Option('--nopipe').hideHelp(true))
|
|
35
|
+
.addOption(new Option('--no-pipe').hideHelp(true));
|
|
27
36
|
const cliConfigOverrides = {};
|
|
28
37
|
// Parse global options before binding any commands
|
|
29
38
|
program.parseOptions(argv);
|
|
@@ -62,9 +71,31 @@ reviewCommand(program, cliConfigOverrides);
|
|
|
62
71
|
prCommand(program, cliConfigOverrides);
|
|
63
72
|
askCommand(program, cliConfigOverrides);
|
|
64
73
|
execCommand(program, cliConfigOverrides);
|
|
74
|
+
batchCommand(program, cliConfigOverrides);
|
|
75
|
+
evalCommand(program, cliConfigOverrides);
|
|
76
|
+
workflowCommand(program, cliConfigOverrides);
|
|
65
77
|
chatCommand(program, cliConfigOverrides);
|
|
66
78
|
codeCommand(program, cliConfigOverrides);
|
|
67
79
|
apiCommand(program, cliConfigOverrides);
|
|
68
80
|
getCommand(program, cliConfigOverrides);
|
|
81
|
+
configCommand(program, cliConfigOverrides);
|
|
82
|
+
// GS2-7 (B20) — read-only, local history/insights surfaces. They resolve their own DB path (global
|
|
83
|
+
// default or --db) and do not build the LLM, so they stay decoupled from config/provider setup.
|
|
84
|
+
historyCommand(program);
|
|
85
|
+
insightsCommand(program);
|
|
86
|
+
// GS2-6 (B16) — model catalog: lists providers/models enriched with models.dev cost/limit metadata.
|
|
87
|
+
// Read-only; enrichment never gates what `/v1/models` reports as callable.
|
|
88
|
+
modelsCommand(program);
|
|
89
|
+
// BATCH-11 (#405 gotcha #5): `eval`/`batch` never consume piped stdin, so they must not block
|
|
90
|
+
// waiting for stdin EOF before dispatch — a scripted/CI `gth eval suite.yaml` inherits a non-TTY,
|
|
91
|
+
// non-closing stdin and would otherwise hang until EOF (or need `</dev/null`). Resolve the invoked
|
|
92
|
+
// subcommand from argv via commander's own operand parsing (so a `-c <path>` value or a file
|
|
93
|
+
// argument is never mistaken for the command name) and, for those commands, imply the existing
|
|
94
|
+
// `--no-pipe` fast path in readStdin. ask/review/pr etc. are untouched and still block-and-read a
|
|
95
|
+
// piped diff.
|
|
96
|
+
const invokedCommand = resolveInvokedCommandName(program.commands.map((command) => command.name()), program.parseOptions(argv).operands);
|
|
97
|
+
if (commandSkipsStdin(invokedCommand)) {
|
|
98
|
+
program.setOptionValue('nopipe', true);
|
|
99
|
+
}
|
|
69
100
|
await readStdin(program);
|
|
70
101
|
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAC;AACvD,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAC3D,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,wCAAwC,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module adkEvalRunner
|
|
3
|
+
* BATCH-14 — the ADK (A2A) target's production runner builders for `gth eval`. The eval runner
|
|
4
|
+
* ({@link @gaunt-sloth/batch#runEvalSuite}) is target-agnostic: it consumes an injected
|
|
5
|
+
* {@link RunCellFn} (single-shot) and {@link RunConversationFn} (multi-turn) and grades whatever
|
|
6
|
+
* `answer` they produce with the SAME assertion surface used for the `gth-agent` target. This module
|
|
7
|
+
* builds those two functions for an EXTERNAL Google ADK agent, driving it over the A2A protocol via
|
|
8
|
+
* {@link A2AClientWrapper} (the existing `@a2a-js/sdk` wrapper) — the analogue of `batchCommand.ts`'s
|
|
9
|
+
* `buildProductionRunCell`/`buildProductionRunConversation` for the in-process gth agent.
|
|
10
|
+
*
|
|
11
|
+
* The A2A client is INJECTABLE (like `RunCellFn`/`RunConversationFn` themselves) so unit tests drive
|
|
12
|
+
* these builders against a FAKE client with no network and no live ADK agent — the live end-to-end
|
|
13
|
+
* validation is a separate node (BATCH-16).
|
|
14
|
+
*
|
|
15
|
+
* Honest boundaries (BATCH-14 design point 4): A2A exposes text (and file/data) parts plus task
|
|
16
|
+
* status/artifact events, but NOT the agent's intermediate tool/function calls — so the ADK outcomes
|
|
17
|
+
* never populate `tools`, and `must_call`/`must_not_call` are rejected at suite-parse time rather
|
|
18
|
+
* than silently passed. A2A likewise carries no token accounting, so `tokensInput`/`tokensOutput`
|
|
19
|
+
* are left unset (undefined), unlike the gth-agent path.
|
|
20
|
+
*/
|
|
21
|
+
import type { AdkAgentTarget, RunCellFn, RunConversationFn } from '@gaunt-sloth/batch';
|
|
22
|
+
/** One A2A turn's result as the ADK runner needs it: the answer text plus the continuity handles to
|
|
23
|
+
* thread into follow-up turns. */
|
|
24
|
+
export interface AdkA2AResponse {
|
|
25
|
+
text: string;
|
|
26
|
+
contextId?: string;
|
|
27
|
+
taskId?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The minimal A2A client the ADK runner depends on — a single `sendMessage(text, context?)` that
|
|
31
|
+
* returns text + continuity handles. Production wraps {@link A2AClientWrapper.sendMessageWithContext};
|
|
32
|
+
* tests inject a fake implementing exactly this. Kept deliberately narrow so a fake is trivial and
|
|
33
|
+
* the runner never reaches into the full SDK surface.
|
|
34
|
+
*/
|
|
35
|
+
export interface AdkA2AClient {
|
|
36
|
+
sendMessage(text: string, context?: {
|
|
37
|
+
contextId?: string;
|
|
38
|
+
taskId?: string;
|
|
39
|
+
}): Promise<AdkA2AResponse>;
|
|
40
|
+
}
|
|
41
|
+
/** Builds an {@link AdkA2AClient} for a target — the injection seam for tests. */
|
|
42
|
+
export type AdkClientFactory = (target: AdkAgentTarget) => AdkA2AClient;
|
|
43
|
+
/** Production factory: a real {@link A2AClientWrapper} over the target's A2A endpoint, adapted to the
|
|
44
|
+
* narrow {@link AdkA2AClient} shape via its context-threading {@link A2AClientWrapper.sendMessageWithContext}. */
|
|
45
|
+
export declare const defaultAdkClientFactory: AdkClientFactory;
|
|
46
|
+
/**
|
|
47
|
+
* Build the injectable single-shot {@link RunCellFn} that drives ONE ADK agent turn over A2A: send
|
|
48
|
+
* the cell's prompt as the A2A message text and return the agent's text as the cell `answer`. A
|
|
49
|
+
* transport/agent error is contained as a failed cell (`ok:false`) so one bad case can never take the
|
|
50
|
+
* whole suite down — matching `buildProductionRunCell`'s discipline for the gth-agent path.
|
|
51
|
+
*
|
|
52
|
+
* `tools`/tokens are intentionally unset (A2A exposes neither); content assertions grade the answer.
|
|
53
|
+
*/
|
|
54
|
+
export declare function buildAdkRunCell(target: AdkAgentTarget, createClient?: AdkClientFactory): RunCellFn;
|
|
55
|
+
/**
|
|
56
|
+
* Build the injectable multi-turn {@link RunConversationFn} that drives a whole scripted conversation
|
|
57
|
+
* against the ADK agent over A2A, threading the A2A `contextId` across turns so turn N sees turn
|
|
58
|
+
* N-1's context (an ADK agent keeps conversational memory by `contextId`). ONE client is built for
|
|
59
|
+
* the conversation; the first turn sends no context, and each subsequent turn carries the `contextId`
|
|
60
|
+
* the previous response returned.
|
|
61
|
+
*
|
|
62
|
+
* Returns one {@link TurnRunOutcome} per turn attempted. A turn that throws is recorded as a failed
|
|
63
|
+
* turn and ABORTS the conversation (the returned array is short) — the runner fails the un-run turns
|
|
64
|
+
* with a clear reason, exactly as it does for a gth-agent conversation that ended early.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildAdkRunConversation(target: AdkAgentTarget, createClient?: AdkClientFactory): RunConversationFn;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { A2AClientWrapper } from '#src/modules/a2a/A2AClientWrapper.js';
|
|
2
|
+
/** Production factory: a real {@link A2AClientWrapper} over the target's A2A endpoint, adapted to the
|
|
3
|
+
* narrow {@link AdkA2AClient} shape via its context-threading {@link A2AClientWrapper.sendMessageWithContext}. */
|
|
4
|
+
export const defaultAdkClientFactory = (target) => {
|
|
5
|
+
const wrapper = new A2AClientWrapper({
|
|
6
|
+
agentId: target.agentId ?? 'adk-agent',
|
|
7
|
+
agentUrl: target.url,
|
|
8
|
+
});
|
|
9
|
+
return {
|
|
10
|
+
sendMessage: (text, context) => wrapper.sendMessageWithContext(text, context),
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Build the injectable single-shot {@link RunCellFn} that drives ONE ADK agent turn over A2A: send
|
|
15
|
+
* the cell's prompt as the A2A message text and return the agent's text as the cell `answer`. A
|
|
16
|
+
* transport/agent error is contained as a failed cell (`ok:false`) so one bad case can never take the
|
|
17
|
+
* whole suite down — matching `buildProductionRunCell`'s discipline for the gth-agent path.
|
|
18
|
+
*
|
|
19
|
+
* `tools`/tokens are intentionally unset (A2A exposes neither); content assertions grade the answer.
|
|
20
|
+
*/
|
|
21
|
+
export function buildAdkRunCell(target, createClient = defaultAdkClientFactory) {
|
|
22
|
+
return async (cell) => {
|
|
23
|
+
try {
|
|
24
|
+
const client = createClient(target);
|
|
25
|
+
const { text } = await client.sendMessage(cell.content);
|
|
26
|
+
return { ok: true, answer: text };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build the injectable multi-turn {@link RunConversationFn} that drives a whole scripted conversation
|
|
35
|
+
* against the ADK agent over A2A, threading the A2A `contextId` across turns so turn N sees turn
|
|
36
|
+
* N-1's context (an ADK agent keeps conversational memory by `contextId`). ONE client is built for
|
|
37
|
+
* the conversation; the first turn sends no context, and each subsequent turn carries the `contextId`
|
|
38
|
+
* the previous response returned.
|
|
39
|
+
*
|
|
40
|
+
* Returns one {@link TurnRunOutcome} per turn attempted. A turn that throws is recorded as a failed
|
|
41
|
+
* turn and ABORTS the conversation (the returned array is short) — the runner fails the un-run turns
|
|
42
|
+
* with a clear reason, exactly as it does for a gth-agent conversation that ended early.
|
|
43
|
+
*/
|
|
44
|
+
export function buildAdkRunConversation(target, createClient = defaultAdkClientFactory) {
|
|
45
|
+
return async (userMessages) => {
|
|
46
|
+
const client = createClient(target);
|
|
47
|
+
const outcomes = [];
|
|
48
|
+
let contextId;
|
|
49
|
+
for (const message of userMessages) {
|
|
50
|
+
try {
|
|
51
|
+
const response = await client.sendMessage(message, contextId ? { contextId } : undefined);
|
|
52
|
+
// Carry the server's contextId forward so the next turn continues the same conversation.
|
|
53
|
+
if (response.contextId)
|
|
54
|
+
contextId = response.contextId;
|
|
55
|
+
outcomes.push({ ok: true, answer: response.text });
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
outcomes.push({
|
|
59
|
+
ok: false,
|
|
60
|
+
error: error instanceof Error ? error.message : String(error),
|
|
61
|
+
});
|
|
62
|
+
// A failed turn aborts the conversation — the runner marks the remaining turns FAILed.
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return outcomes;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=adkEvalRunner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adkEvalRunner.js","sourceRoot":"","sources":["../../src/commands/adkEvalRunner.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AA0BxE;kHACkH;AAClH,MAAM,CAAC,MAAM,uBAAuB,GAAqB,CAAC,MAAM,EAAE,EAAE;IAClE,MAAM,OAAO,GAAG,IAAI,gBAAgB,CAAC;QACnC,OAAO,EAAE,MAAM,CAAC,OAAO,IAAI,WAAW;QACtC,QAAQ,EAAE,MAAM,CAAC,GAAG;KACrB,CAAC,CAAC;IACH,OAAO;QACL,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,sBAAsB,CAAC,IAAI,EAAE,OAAO,CAAC;KAC9E,CAAC;AACJ,CAAC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAsB,EACtB,YAAY,GAAqB,uBAAuB;IAExD,OAAO,KAAK,EAAE,IAAI,EAAE,EAAE;QACpB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;YACpC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;YACxD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;QACpC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QACtF,CAAC;IACH,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,uBAAuB,CACrC,MAAsB,EACtB,YAAY,GAAqB,uBAAuB;IAExD,OAAO,KAAK,EAAE,YAAY,EAAE,EAAE;QAC5B,MAAM,MAAM,GAAG,YAAY,CAAC,MAAM,CAAC,CAAC;QACpC,MAAM,QAAQ,GAAqB,EAAE,CAAC;QACtC,IAAI,SAA6B,CAAC;QAElC,KAAK,MAAM,OAAO,IAAI,YAAY,EAAE,CAAC;YACnC,IAAI,CAAC;gBACH,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;gBAC1F,yFAAyF;gBACzF,IAAI,QAAQ,CAAC,SAAS;oBAAE,SAAS,GAAG,QAAQ,CAAC,SAAS,CAAC;gBACvD,QAAQ,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;YACrD,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,QAAQ,CAAC,IAAI,CAAC;oBACZ,EAAE,EAAE,KAAK;oBACT,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC;iBAC9D,CAAC,CAAC;gBACH,uFAAuF;gBACvF,MAAM;YACR,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AgUiAgentTarget, RunCellFn, RunConversationFn } from '@gaunt-sloth/batch';
|
|
2
|
+
/** One AG-UI message as sent in the `RunAgentInput.messages` array. The eval only ever sends `user`
|
|
3
|
+
* turns and, for conversation continuity, the `assistant` answers it assembled — the minimal shape
|
|
4
|
+
* the reference server's `convertMessage` consumes (`{ id, role, content }`). */
|
|
5
|
+
export interface AgUiMessage {
|
|
6
|
+
id: string;
|
|
7
|
+
role: 'user' | 'assistant';
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
/** The `RunAgentInput` fields the runner drives per request. `threadId` provides conversation
|
|
11
|
+
* continuity (stable across the turns of ONE conversation); `runId` is fresh per request. */
|
|
12
|
+
export interface AgUiRunInput {
|
|
13
|
+
threadId: string;
|
|
14
|
+
runId: string;
|
|
15
|
+
messages: AgUiMessage[];
|
|
16
|
+
}
|
|
17
|
+
/** One AG-UI run's decoded result: the answer assembled from `TEXT_MESSAGE_CONTENT` deltas and the
|
|
18
|
+
* tool names captured from `TOOL_CALL_START` events (empty array when the agent called no tools —
|
|
19
|
+
* NOT undefined, so `must_not_call` grades against a real, present trace). */
|
|
20
|
+
export interface AgUiRunResult {
|
|
21
|
+
answer: string;
|
|
22
|
+
tools: string[];
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The minimal AG-UI client the runner depends on — a single `run(input)` that POSTs one
|
|
26
|
+
* `RunAgentInput` and returns the decoded answer + tools. Production wraps {@link createAgUiClient};
|
|
27
|
+
* tests inject a fake implementing exactly this. Kept deliberately narrow so a fake is trivial and
|
|
28
|
+
* the runner never reaches into transport details.
|
|
29
|
+
*/
|
|
30
|
+
export interface AgUiClient {
|
|
31
|
+
run(input: AgUiRunInput): Promise<AgUiRunResult>;
|
|
32
|
+
}
|
|
33
|
+
/** Builds an {@link AgUiClient} for a target — the injection seam for tests. */
|
|
34
|
+
export type AgUiClientFactory = (target: AgUiAgentTarget) => AgUiClient;
|
|
35
|
+
/** The `fetch` seam — global `fetch` in production, a fake in tests (so the real POST-body
|
|
36
|
+
* construction + SSE decode are exercised with no network). */
|
|
37
|
+
export type FetchLike = typeof fetch;
|
|
38
|
+
/**
|
|
39
|
+
* Create a real {@link AgUiClient} that drives the target's AG-UI run endpoint over HTTP/SSE. Each
|
|
40
|
+
* `run` POSTs a `RunAgentInput` to `{url}/agents/{agentId}/run` and decodes the SSE response via
|
|
41
|
+
* {@link decodeAgUiStream}. A non-2xx response, an absent body, a `RUN_ERROR` event, or a truncated
|
|
42
|
+
* stream all throw — the runner builders contain that into a failed cell (`ok:false`).
|
|
43
|
+
*
|
|
44
|
+
* `fetchImpl` defaults to global `fetch`; tests pass a fake returning a synthetic SSE `Response` so
|
|
45
|
+
* the body construction + decode are exercised without a network or a live server.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createAgUiClient(target: AgUiAgentTarget, fetchImpl?: FetchLike): AgUiClient;
|
|
48
|
+
/** Production factory: a real HTTP/SSE {@link AgUiClient} over the target's AG-UI endpoint. */
|
|
49
|
+
export declare const defaultAgUiClientFactory: AgUiClientFactory;
|
|
50
|
+
/**
|
|
51
|
+
* Build the injectable single-shot {@link RunCellFn} that drives ONE AG-UI run: send the cell's
|
|
52
|
+
* prompt as the sole `user` message (fresh `threadId`/`runId`) and return the agent's assembled text
|
|
53
|
+
* as the cell `answer` plus the tool names captured from the stream as `tools`. A transport/stream
|
|
54
|
+
* error is contained as a failed cell (`ok:false`) so one bad case can never take the whole suite
|
|
55
|
+
* down — matching `buildProductionRunCell`'s discipline for the gth-agent path.
|
|
56
|
+
*
|
|
57
|
+
* `tools` is ALWAYS populated (possibly `[]`) — the AG-UI wire exposes the trace — so
|
|
58
|
+
* `must_call`/`must_not_call` grade normally, the key difference from the ADK target.
|
|
59
|
+
*/
|
|
60
|
+
export declare function buildAgUiRunCell(target: AgUiAgentTarget, createClient?: AgUiClientFactory): RunCellFn;
|
|
61
|
+
/**
|
|
62
|
+
* Build the injectable multi-turn {@link RunConversationFn} that drives a whole scripted conversation
|
|
63
|
+
* against the AG-UI agent, threading continuity via the `messages` array + a STABLE `threadId` across
|
|
64
|
+
* the turns of ONE conversation: each turn appends its `user` message, POSTs the accumulated history
|
|
65
|
+
* under the same `threadId`, then appends the assistant's answer so the next turn's request carries
|
|
66
|
+
* it. ONE `threadId` for the whole conversation; a fresh `runId` per turn.
|
|
67
|
+
*
|
|
68
|
+
* Returns one {@link TurnRunOutcome} per turn attempted (answer + per-turn tool trace). A turn that
|
|
69
|
+
* throws is recorded as a failed turn and ABORTS the conversation (the returned array is short) — the
|
|
70
|
+
* runner fails the un-run turns with a clear reason, exactly as it does for a gth-agent conversation
|
|
71
|
+
* that ended early.
|
|
72
|
+
*/
|
|
73
|
+
export declare function buildAgUiRunConversation(target: AgUiAgentTarget, createClient?: AgUiClientFactory): RunConversationFn;
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module agUiEvalRunner
|
|
3
|
+
* BATCH-15 — the AG-UI target's production runner builders for `gth eval`. The eval runner
|
|
4
|
+
* ({@link @gaunt-sloth/batch#runEvalSuite}) is target-agnostic: it consumes an injected
|
|
5
|
+
* {@link RunCellFn} (single-shot) and {@link RunConversationFn} (multi-turn) and grades whatever
|
|
6
|
+
* `answer` (and `tools`) they produce with the SAME assertion surface used for the `gth-agent`
|
|
7
|
+
* target. This module builds those two functions for an EXTERNAL agent exposed over the AG-UI
|
|
8
|
+
* protocol, driving its HTTP/SSE run endpoint — the analogue of `batchCommand.ts`'s
|
|
9
|
+
* `buildProductionRunCell`/`buildProductionRunConversation` (in-process gth agent) and of
|
|
10
|
+
* `adkEvalRunner.ts` (external ADK agent over A2A).
|
|
11
|
+
*
|
|
12
|
+
* The transport is INJECTABLE (like `RunCellFn`/`RunConversationFn` themselves) so unit tests drive
|
|
13
|
+
* these builders against a FAKE client — or the real decoder against a FAKE `fetch` — with no
|
|
14
|
+
* network and no live AG-UI server. The live end-to-end validation against a running `gth api ag-ui`
|
|
15
|
+
* server is a separate node (BATCH-17).
|
|
16
|
+
*
|
|
17
|
+
* KEY DIFFERENCE from the ADK (A2A) runner: the AG-UI wire DOES stream the agent's tool calls
|
|
18
|
+
* (`TOOL_CALL_START`). So this runner CAPTURES each `TOOL_CALL_START`'s `toolCallName` into the
|
|
19
|
+
* outcome's `tools`, and `must_call`/`must_not_call` grade normally — unlike the adk-agent target,
|
|
20
|
+
* where the tool trace is invisible and those assertions are rejected at parse time. AG-UI carries
|
|
21
|
+
* no token accounting either, so `tokensInput`/`tokensOutput` are left unset (undefined).
|
|
22
|
+
*
|
|
23
|
+
* The wire contract (see the reference endpoint, `@gaunt-sloth/api`'s `apiAgUiModule.ts`):
|
|
24
|
+
* - Request: `POST {url}/agents/{agentId}/run` with a `RunAgentInput` body
|
|
25
|
+
* `{ threadId, runId, messages, tools, forwardedProps }`.
|
|
26
|
+
* - Response: an SSE stream of AG-UI events. We decode `TEXT_MESSAGE_CONTENT` deltas into the
|
|
27
|
+
* `answer`, `TOOL_CALL_START` names into `tools`, and treat `RUN_ERROR` (or a non-200, or a stream
|
|
28
|
+
* that ends without the terminal `RUN_FINISHED`) as a failed run — a gradeable `ok:false`, never
|
|
29
|
+
* an uncaught throw that aborts the whole suite.
|
|
30
|
+
*/
|
|
31
|
+
import { randomUUID } from 'node:crypto';
|
|
32
|
+
import { EventType } from '@ag-ui/core';
|
|
33
|
+
/**
|
|
34
|
+
* Decode an AG-UI SSE stream into an {@link AgUiRunResult}. The reference encoder frames each event
|
|
35
|
+
* as `data: <json>\n\n` (`@ag-ui/encoder`), so we split on the blank-line delimiter, JSON-parse each
|
|
36
|
+
* frame's `data:` payload, and fold the relevant event types:
|
|
37
|
+
* - `TEXT_MESSAGE_CONTENT` → append `delta` to the answer.
|
|
38
|
+
* - `TOOL_CALL_START` → capture `toolCallName` into `tools`.
|
|
39
|
+
* - `RUN_ERROR` → remember the message; the run FAILED.
|
|
40
|
+
* - `RUN_FINISHED` → the terminal success signal.
|
|
41
|
+
*
|
|
42
|
+
* Throws on `RUN_ERROR` OR on a stream that ends without a terminal `RUN_FINISHED` (a truncated /
|
|
43
|
+
* malformed stream is a failed run, not an "empty answer" success). Non-JSON frames (SSE comments /
|
|
44
|
+
* keep-alives) are ignored. The throw is contained by the runner builders below into `ok:false`.
|
|
45
|
+
*/
|
|
46
|
+
async function decodeAgUiStream(body) {
|
|
47
|
+
const decoder = new TextDecoder();
|
|
48
|
+
let buffer = '';
|
|
49
|
+
let answer = '';
|
|
50
|
+
const tools = [];
|
|
51
|
+
let runError;
|
|
52
|
+
let sawRunFinished = false;
|
|
53
|
+
const handleFrame = (frame) => {
|
|
54
|
+
const payload = frame
|
|
55
|
+
.split(/\r?\n/)
|
|
56
|
+
.filter((line) => line.startsWith('data:'))
|
|
57
|
+
.map((line) => line.slice('data:'.length).trim())
|
|
58
|
+
.join('\n');
|
|
59
|
+
if (!payload)
|
|
60
|
+
return;
|
|
61
|
+
let event;
|
|
62
|
+
try {
|
|
63
|
+
event = JSON.parse(payload);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Non-JSON frame (SSE comment / keep-alive) — ignore.
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
switch (event.type) {
|
|
70
|
+
case EventType.TEXT_MESSAGE_CONTENT:
|
|
71
|
+
if (typeof event.delta === 'string')
|
|
72
|
+
answer += event.delta;
|
|
73
|
+
break;
|
|
74
|
+
case EventType.TOOL_CALL_START:
|
|
75
|
+
if (typeof event.toolCallName === 'string')
|
|
76
|
+
tools.push(event.toolCallName);
|
|
77
|
+
break;
|
|
78
|
+
case EventType.RUN_ERROR:
|
|
79
|
+
runError = typeof event.message === 'string' ? event.message : 'unknown run error';
|
|
80
|
+
break;
|
|
81
|
+
case EventType.RUN_FINISHED:
|
|
82
|
+
sawRunFinished = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
for await (const chunk of body) {
|
|
87
|
+
buffer += decoder.decode(chunk, { stream: true });
|
|
88
|
+
let sep;
|
|
89
|
+
while ((sep = buffer.indexOf('\n\n')) !== -1) {
|
|
90
|
+
handleFrame(buffer.slice(0, sep));
|
|
91
|
+
buffer = buffer.slice(sep + 2);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
buffer += decoder.decode();
|
|
95
|
+
if (buffer.trim().length > 0)
|
|
96
|
+
handleFrame(buffer);
|
|
97
|
+
if (runError !== undefined) {
|
|
98
|
+
throw new Error(`AG-UI run error: ${runError}`);
|
|
99
|
+
}
|
|
100
|
+
if (!sawRunFinished) {
|
|
101
|
+
throw new Error('AG-UI stream ended without a terminal RUN_FINISHED event (truncated or malformed stream).');
|
|
102
|
+
}
|
|
103
|
+
return { answer, tools };
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Create a real {@link AgUiClient} that drives the target's AG-UI run endpoint over HTTP/SSE. Each
|
|
107
|
+
* `run` POSTs a `RunAgentInput` to `{url}/agents/{agentId}/run` and decodes the SSE response via
|
|
108
|
+
* {@link decodeAgUiStream}. A non-2xx response, an absent body, a `RUN_ERROR` event, or a truncated
|
|
109
|
+
* stream all throw — the runner builders contain that into a failed cell (`ok:false`).
|
|
110
|
+
*
|
|
111
|
+
* `fetchImpl` defaults to global `fetch`; tests pass a fake returning a synthetic SSE `Response` so
|
|
112
|
+
* the body construction + decode are exercised without a network or a live server.
|
|
113
|
+
*/
|
|
114
|
+
export function createAgUiClient(target, fetchImpl = fetch) {
|
|
115
|
+
const base = target.url.replace(/\/+$/, '');
|
|
116
|
+
const endpoint = `${base}/agents/${encodeURIComponent(target.agentId)}/run`;
|
|
117
|
+
return {
|
|
118
|
+
async run(input) {
|
|
119
|
+
const response = await fetchImpl(endpoint, {
|
|
120
|
+
method: 'POST',
|
|
121
|
+
headers: { 'Content-Type': 'application/json', Accept: 'text/event-stream' },
|
|
122
|
+
body: JSON.stringify({
|
|
123
|
+
threadId: input.threadId,
|
|
124
|
+
runId: input.runId,
|
|
125
|
+
messages: input.messages,
|
|
126
|
+
// The eval declares no client-fulfilled frontend tools; an empty array makes the server
|
|
127
|
+
// serve the run from its own statically-configured agent.
|
|
128
|
+
tools: [],
|
|
129
|
+
forwardedProps: {},
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
if (!response.ok) {
|
|
133
|
+
throw new Error(`AG-UI server responded ${response.status} ${response.statusText || ''}`.trim());
|
|
134
|
+
}
|
|
135
|
+
if (!response.body) {
|
|
136
|
+
throw new Error('AG-UI server returned no response body to stream.');
|
|
137
|
+
}
|
|
138
|
+
return decodeAgUiStream(response.body);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Production factory: a real HTTP/SSE {@link AgUiClient} over the target's AG-UI endpoint. */
|
|
143
|
+
export const defaultAgUiClientFactory = (target) => createAgUiClient(target);
|
|
144
|
+
/**
|
|
145
|
+
* Build the injectable single-shot {@link RunCellFn} that drives ONE AG-UI run: send the cell's
|
|
146
|
+
* prompt as the sole `user` message (fresh `threadId`/`runId`) and return the agent's assembled text
|
|
147
|
+
* as the cell `answer` plus the tool names captured from the stream as `tools`. A transport/stream
|
|
148
|
+
* error is contained as a failed cell (`ok:false`) so one bad case can never take the whole suite
|
|
149
|
+
* down — matching `buildProductionRunCell`'s discipline for the gth-agent path.
|
|
150
|
+
*
|
|
151
|
+
* `tools` is ALWAYS populated (possibly `[]`) — the AG-UI wire exposes the trace — so
|
|
152
|
+
* `must_call`/`must_not_call` grade normally, the key difference from the ADK target.
|
|
153
|
+
*/
|
|
154
|
+
export function buildAgUiRunCell(target, createClient = defaultAgUiClientFactory) {
|
|
155
|
+
return async (cell) => {
|
|
156
|
+
try {
|
|
157
|
+
const client = createClient(target);
|
|
158
|
+
const result = await client.run({
|
|
159
|
+
threadId: randomUUID(),
|
|
160
|
+
runId: randomUUID(),
|
|
161
|
+
messages: [{ id: randomUUID(), role: 'user', content: cell.content }],
|
|
162
|
+
});
|
|
163
|
+
return { ok: true, answer: result.answer, tools: result.tools };
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Build the injectable multi-turn {@link RunConversationFn} that drives a whole scripted conversation
|
|
172
|
+
* against the AG-UI agent, threading continuity via the `messages` array + a STABLE `threadId` across
|
|
173
|
+
* the turns of ONE conversation: each turn appends its `user` message, POSTs the accumulated history
|
|
174
|
+
* under the same `threadId`, then appends the assistant's answer so the next turn's request carries
|
|
175
|
+
* it. ONE `threadId` for the whole conversation; a fresh `runId` per turn.
|
|
176
|
+
*
|
|
177
|
+
* Returns one {@link TurnRunOutcome} per turn attempted (answer + per-turn tool trace). A turn that
|
|
178
|
+
* throws is recorded as a failed turn and ABORTS the conversation (the returned array is short) — the
|
|
179
|
+
* runner fails the un-run turns with a clear reason, exactly as it does for a gth-agent conversation
|
|
180
|
+
* that ended early.
|
|
181
|
+
*/
|
|
182
|
+
export function buildAgUiRunConversation(target, createClient = defaultAgUiClientFactory) {
|
|
183
|
+
return async (userMessages) => {
|
|
184
|
+
const client = createClient(target);
|
|
185
|
+
const outcomes = [];
|
|
186
|
+
// One stable thread for the whole conversation (continuity), plus the accumulated history the
|
|
187
|
+
// server replays each turn (its `add_messages` reducer dedupes by id, so re-sending is safe).
|
|
188
|
+
const threadId = randomUUID();
|
|
189
|
+
const messages = [];
|
|
190
|
+
for (const userMessage of userMessages) {
|
|
191
|
+
messages.push({ id: randomUUID(), role: 'user', content: userMessage });
|
|
192
|
+
try {
|
|
193
|
+
const result = await client.run({
|
|
194
|
+
threadId,
|
|
195
|
+
runId: randomUUID(),
|
|
196
|
+
// Snapshot the history so the caller/test sees exactly what THIS turn sent, unaffected by
|
|
197
|
+
// later mutation.
|
|
198
|
+
messages: [...messages],
|
|
199
|
+
});
|
|
200
|
+
// Thread the assistant's answer into the history so the next turn carries it (memory).
|
|
201
|
+
messages.push({ id: randomUUID(), role: 'assistant', content: result.answer });
|
|
202
|
+
outcomes.push({ ok: true, answer: result.answer, tools: result.tools });
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
outcomes.push({
|
|
206
|
+
ok: false,
|
|
207
|
+
error: error instanceof Error ? error.message : String(error),
|
|
208
|
+
});
|
|
209
|
+
// A failed turn aborts the conversation — the runner marks the remaining turns FAILed.
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return outcomes;
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
//# sourceMappingURL=agUiEvalRunner.js.map
|