@gajae-code/coding-agent 0.5.0 → 0.5.1
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 +19 -0
- package/dist/types/async/job-manager.d.ts +26 -0
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/cli/list-models.d.ts +6 -0
- package/dist/types/commands/gc.d.ts +26 -0
- package/dist/types/config/file-lock-gc.d.ts +5 -0
- package/dist/types/config/file-lock.d.ts +7 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/defaults/gjc/extensions/grok-build/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/index.d.ts +1 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.d.ts +25 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.d.ts +27 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.d.ts +8 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.d.ts +5 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.d.ts +10 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.d.ts +2 -0
- package/dist/types/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.d.ts +38 -0
- package/dist/types/defaults/gjc-grok-cli.d.ts +5 -0
- package/dist/types/extensibility/extensions/index.d.ts +1 -0
- package/dist/types/extensibility/extensions/prefix-command-bridge.d.ts +35 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +103 -0
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +112 -0
- package/dist/types/gjc-runtime/gc-render.d.ts +6 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +134 -0
- package/dist/types/gjc-runtime/ledger-event-renderer.d.ts +68 -0
- package/dist/types/gjc-runtime/team-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +5 -0
- package/dist/types/gjc-runtime/tmux-common.d.ts +11 -0
- package/dist/types/gjc-runtime/tmux-gc.d.ts +7 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +13 -0
- package/dist/types/harness-control-plane/gc-adapter.d.ts +3 -0
- package/dist/types/harness-control-plane/owner.d.ts +7 -0
- package/dist/types/harness-control-plane/storage.d.ts +20 -0
- package/dist/types/modes/components/hook-selector.d.ts +7 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +16 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +13 -0
- package/dist/types/modes/shared/agent-wire/session-registry.d.ts +25 -0
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +39 -3
- package/dist/types/skill-state/workflow-hud.d.ts +14 -0
- package/dist/types/tools/ask.d.ts +15 -1
- package/dist/types/tools/subagent.d.ts +6 -0
- package/package.json +7 -7
- package/src/async/job-manager.ts +52 -0
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-broker-cli.ts +1 -0
- package/src/cli/list-models.ts +13 -1
- package/src/cli.ts +1 -0
- package/src/commands/gc.ts +22 -0
- package/src/commands/harness.ts +7 -3
- package/src/config/file-lock-gc.ts +181 -0
- package/src/config/file-lock.ts +14 -0
- package/src/config/model-profiles.ts +24 -15
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/server.ts +459 -3
- package/src/defaults/gjc/agent.models.grok-cli.yml +36 -0
- package/src/defaults/gjc/extensions/grok-build/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-build/package.json +7 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/biome.json +39 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/package.json +8 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/index.ts +1 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +155 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/payload/sanitize.ts +361 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/billing.ts +57 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/register.ts +99 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/stream.ts +50 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/provider/usage.ts +56 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +36 -0
- package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/errors.ts +44 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +131 -113
- package/src/defaults/gjc/skills/deep-interview/lateral-review-panel.md +49 -0
- package/src/defaults/gjc-defaults.ts +7 -0
- package/src/defaults/gjc-grok-cli.ts +22 -0
- package/src/extensibility/extensions/index.ts +1 -0
- package/src/extensibility/extensions/prefix-command-bridge.ts +128 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +417 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +18 -26
- package/src/gjc-runtime/deep-interview-state.ts +324 -0
- package/src/gjc-runtime/gc-render.ts +70 -0
- package/src/gjc-runtime/gc-runtime.ts +403 -0
- package/src/gjc-runtime/ledger-event-renderer.ts +164 -0
- package/src/gjc-runtime/ralplan-runtime.ts +58 -7
- package/src/gjc-runtime/state-renderer.ts +12 -3
- package/src/gjc-runtime/state-runtime.ts +46 -29
- package/src/gjc-runtime/team-gc.ts +49 -0
- package/src/gjc-runtime/team-runtime.ts +179 -2
- package/src/gjc-runtime/tmux-common.ts +14 -0
- package/src/gjc-runtime/tmux-gc.ts +176 -0
- package/src/gjc-runtime/tmux-sessions.ts +49 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +12 -0
- package/src/harness-control-plane/gc-adapter.ts +184 -0
- package/src/harness-control-plane/owner.ts +11 -0
- package/src/harness-control-plane/storage.ts +70 -0
- package/src/internal-urls/docs-index.generated.ts +14 -8
- package/src/main.ts +7 -2
- package/src/modes/components/hook-selector.ts +19 -0
- package/src/modes/components/model-selector.ts +25 -8
- package/src/modes/components/status-line/segments.ts +1 -1
- package/src/modes/controllers/command-controller.ts +25 -6
- package/src/modes/controllers/extension-ui-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/rpc/rpc-mode.ts +151 -33
- package/src/modes/shared/agent-wire/command-dispatch.ts +278 -261
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +30 -1
- package/src/modes/shared/agent-wire/session-registry.ts +109 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +24 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +23 -3
- package/src/modes/shared/agent-wire/unattended-session.ts +16 -1
- package/src/sdk.ts +17 -3
- package/src/session/agent-session.ts +77 -8
- package/src/session/blob-store.ts +59 -3
- package/src/session/session-manager.ts +4 -4
- package/src/setup/hermes/templates/operator-instructions.v1.md +7 -1
- package/src/skill-state/workflow-hud.ts +106 -10
- package/src/slash-commands/builtin-registry.ts +3 -2
- package/src/task/executor.ts +9 -0
- package/src/tools/ask.ts +56 -1
- package/src/tools/job.ts +3 -2
- package/src/tools/monitor.ts +36 -1
- package/src/tools/subagent-render.ts +9 -0
- package/src/tools/subagent.ts +26 -2
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-process registry of running gjc RPC sessions (issue 10).
|
|
3
|
+
*
|
|
4
|
+
* Each live RPC server writes a record under `<agent-dir>/rpc-sessions/<id>.json`
|
|
5
|
+
* on start and removes it on shutdown, so a separate process can discover which
|
|
6
|
+
* sessions are alive (and, once persistence lands in issue 09, how to reach
|
|
7
|
+
* them). Listing reaps records whose owning process is no longer alive, so a
|
|
8
|
+
* crashed server never leaves a permanent phantom entry.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from "node:fs/promises";
|
|
11
|
+
import * as path from "node:path";
|
|
12
|
+
import { getAgentDir } from "@gajae-code/utils";
|
|
13
|
+
|
|
14
|
+
export type RpcSessionTransport = "stdio" | "bridge" | "socket";
|
|
15
|
+
|
|
16
|
+
export interface RpcSessionRecord {
|
|
17
|
+
sessionId: string;
|
|
18
|
+
pid: number;
|
|
19
|
+
transport: RpcSessionTransport;
|
|
20
|
+
cwd: string;
|
|
21
|
+
model?: string;
|
|
22
|
+
/** ISO-8601 start timestamp. */
|
|
23
|
+
startedAt: string;
|
|
24
|
+
/** Reachable endpoint for persistent transports (issue 09); absent for stdio. */
|
|
25
|
+
endpoint?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Registry directory: `<agent-dir>/rpc-sessions` (honors GJC_CODING_AGENT_DIR via getAgentDir). */
|
|
29
|
+
function rpcSessionsDir(agentDir?: string): string {
|
|
30
|
+
return path.join(agentDir ?? getAgentDir(), "rpc-sessions");
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function recordPath(sessionId: string, agentDir?: string): string {
|
|
34
|
+
return path.join(rpcSessionsDir(agentDir), `${sessionId}.json`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Write (or replace) the registry record for a session. The record is written to
|
|
39
|
+
* a same-directory temp file and atomically renamed into place so a concurrent
|
|
40
|
+
* reader never observes (and reaps) a partially-written record.
|
|
41
|
+
*/
|
|
42
|
+
export async function registerRpcSession(record: RpcSessionRecord, agentDir?: string): Promise<string> {
|
|
43
|
+
const file = recordPath(record.sessionId, agentDir);
|
|
44
|
+
// `.tmp` suffix keeps the staging file out of the `*.json` listing/reaping path.
|
|
45
|
+
const staging = `${file}.${process.pid}.tmp`;
|
|
46
|
+
await Bun.write(staging, JSON.stringify(record));
|
|
47
|
+
await fs.rename(staging, file);
|
|
48
|
+
return file;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Remove a session's registry record. Best-effort: a missing file is not an error. */
|
|
52
|
+
export async function unregisterRpcSession(sessionId: string, agentDir?: string): Promise<void> {
|
|
53
|
+
await fs.rm(recordPath(sessionId, agentDir), { force: true });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function isProcessAlive(pid: number): boolean {
|
|
57
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
58
|
+
try {
|
|
59
|
+
// Signal 0 performs error checking without delivering a signal.
|
|
60
|
+
process.kill(pid, 0);
|
|
61
|
+
return true;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// ESRCH => no such process (dead). EPERM => alive but owned by another user.
|
|
64
|
+
return (err as NodeJS.ErrnoException).code === "EPERM";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseRecord(raw: string): RpcSessionRecord | undefined {
|
|
69
|
+
let obj: Partial<RpcSessionRecord>;
|
|
70
|
+
try {
|
|
71
|
+
obj = JSON.parse(raw) as Partial<RpcSessionRecord>;
|
|
72
|
+
} catch {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
if (typeof obj.sessionId !== "string" || typeof obj.pid !== "number") return undefined;
|
|
76
|
+
return obj as RpcSessionRecord;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* List live RPC sessions, reaping records whose process is gone or whose file is
|
|
81
|
+
* unparseable. Returns records sorted by `startedAt` ascending.
|
|
82
|
+
*/
|
|
83
|
+
export async function listRpcSessions(agentDir?: string): Promise<RpcSessionRecord[]> {
|
|
84
|
+
const dir = rpcSessionsDir(agentDir);
|
|
85
|
+
let entries: string[];
|
|
86
|
+
try {
|
|
87
|
+
entries = await fs.readdir(dir);
|
|
88
|
+
} catch {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const live: RpcSessionRecord[] = [];
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
if (!entry.endsWith(".json")) continue;
|
|
94
|
+
const file = path.join(dir, entry);
|
|
95
|
+
let raw: string;
|
|
96
|
+
try {
|
|
97
|
+
raw = await fs.readFile(file, "utf8");
|
|
98
|
+
} catch {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const record = parseRecord(raw);
|
|
102
|
+
if (!record || !isProcessAlive(record.pid)) {
|
|
103
|
+
await fs.rm(file, { force: true });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
live.push(record);
|
|
107
|
+
}
|
|
108
|
+
return live.sort((a, b) => a.startedAt.localeCompare(b.startedAt));
|
|
109
|
+
}
|
|
@@ -45,6 +45,30 @@ export function actionClassForScope(scope: BridgeCommandScope): RpcUnattendedAct
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
/** Runtime list of every v1 action class — membership-validation source for negotiate (#319). */
|
|
49
|
+
export const RPC_UNATTENDED_ACTION_CLASSES: readonly RpcUnattendedActionClass[] = [
|
|
50
|
+
"command.prompt",
|
|
51
|
+
"command.control",
|
|
52
|
+
"command.bash",
|
|
53
|
+
"command.export",
|
|
54
|
+
"command.session",
|
|
55
|
+
"command.model",
|
|
56
|
+
"command.message_read",
|
|
57
|
+
"command.host_tools",
|
|
58
|
+
"command.host_uri",
|
|
59
|
+
"command.admin",
|
|
60
|
+
"bash.readonly",
|
|
61
|
+
"bash.mutating",
|
|
62
|
+
"bash.destructive",
|
|
63
|
+
"git.force_push",
|
|
64
|
+
"file.delete",
|
|
65
|
+
"file.write",
|
|
66
|
+
"host_tool.invoke",
|
|
67
|
+
"host_uri.read",
|
|
68
|
+
"host_uri.write",
|
|
69
|
+
"auth.login",
|
|
70
|
+
];
|
|
71
|
+
|
|
48
72
|
const READONLY_COMMANDS = new Set([
|
|
49
73
|
"ls",
|
|
50
74
|
"cat",
|
|
@@ -24,7 +24,8 @@ import type {
|
|
|
24
24
|
RpcUnattendedRefusalCode,
|
|
25
25
|
} from "../../rpc/rpc-types";
|
|
26
26
|
import type { BridgeCommandScope } from "./scopes";
|
|
27
|
-
import {
|
|
27
|
+
import { BRIDGE_COMMAND_SCOPES, MANDATORY_FLOOR_COMMAND_SCOPES } from "./scopes";
|
|
28
|
+
import { actionClassForScope, classifyBashAction, RPC_UNATTENDED_ACTION_CLASSES } from "./unattended-action-policy";
|
|
28
29
|
|
|
29
30
|
/** Coordinated abort surfaces invoked exactly once on a budget breach / abort. */
|
|
30
31
|
export interface UnattendedAbortHooks {
|
|
@@ -157,8 +158,11 @@ export class UnattendedRunController {
|
|
|
157
158
|
this.sessionId = ctx.sessionId;
|
|
158
159
|
this.actor = declaration.actor;
|
|
159
160
|
this.budget = budget;
|
|
160
|
-
this.scopes = new Set(declaration.scopes);
|
|
161
|
-
this.actionAllowlist = new Set(
|
|
161
|
+
this.scopes = new Set([...declaration.scopes, ...MANDATORY_FLOOR_COMMAND_SCOPES]);
|
|
162
|
+
this.actionAllowlist = new Set([
|
|
163
|
+
...declaration.action_allowlist,
|
|
164
|
+
...MANDATORY_FLOOR_COMMAND_SCOPES.map(actionClassForScope),
|
|
165
|
+
]);
|
|
162
166
|
this.now = ctx.now ?? Date.now;
|
|
163
167
|
this.audit = ctx.audit;
|
|
164
168
|
this.abortHooks = ctx.abortHooks ?? {};
|
|
@@ -183,6 +187,22 @@ export class UnattendedRunController {
|
|
|
183
187
|
"declaration.action_allowlist must be string[]",
|
|
184
188
|
);
|
|
185
189
|
}
|
|
190
|
+
const unknownScopes = d.scopes.filter(scope => !BRIDGE_COMMAND_SCOPES.includes(scope as BridgeCommandScope));
|
|
191
|
+
if (unknownScopes.length > 0) {
|
|
192
|
+
throw new UnattendedNegotiationError(
|
|
193
|
+
"invalid_unattended_declaration",
|
|
194
|
+
`declaration.scopes contains unknown scope(s): ${unknownScopes.join(", ")}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
const unknownActions = d.action_allowlist.filter(
|
|
198
|
+
action => !RPC_UNATTENDED_ACTION_CLASSES.includes(action as RpcUnattendedActionClass),
|
|
199
|
+
);
|
|
200
|
+
if (unknownActions.length > 0) {
|
|
201
|
+
throw new UnattendedNegotiationError(
|
|
202
|
+
"invalid_unattended_declaration",
|
|
203
|
+
`declaration.action_allowlist contains unknown action class(es): ${unknownActions.join(", ")}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
186
206
|
const budget = validateBudget(d.budget);
|
|
187
207
|
// Reject providers that cannot account for tokens/cost (fail-closed): require
|
|
188
208
|
// an explicit positive capability signal — omitted/unknown is refused too.
|
|
@@ -31,6 +31,13 @@ import {
|
|
|
31
31
|
} from "./unattended-run-controller";
|
|
32
32
|
import { type GateStore, MemoryGateStore, type OpenGateInput, WorkflowGateBroker } from "./workflow-gate-broker";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* RPC commands that perform agent/tool work and therefore consume one unit of the
|
|
36
|
+
* `max_tool_calls` budget. Read-only/control/cancellation commands are wall-time-bounded
|
|
37
|
+
* and scope-checked but must NOT charge the tool-call budget (issue 04).
|
|
38
|
+
*/
|
|
39
|
+
const CHARGED_COMMAND_TYPES = new Set<RpcCommand["type"]>(["bash", "prompt", "steer", "follow_up", "abort_and_prompt"]);
|
|
40
|
+
|
|
34
41
|
/** Minimal surface a skill runtime / ask tool uses to emit a gate and await its answer. */
|
|
35
42
|
export interface WorkflowGateEmitter {
|
|
36
43
|
/** True only when unattended mode has been negotiated. */
|
|
@@ -116,7 +123,15 @@ export class UnattendedSessionControlPlane implements RpcUnattendedControlPlane,
|
|
|
116
123
|
|
|
117
124
|
preflightCommand(command: RpcCommand): void {
|
|
118
125
|
if (!this.#controller) return;
|
|
119
|
-
|
|
126
|
+
const phase = `${command.type} preflight`;
|
|
127
|
+
// Always enforce wall-time; only charge the tool-call budget for commands that perform
|
|
128
|
+
// agent/tool work (issue 04). Read-only/control/cancellation commands must not consume
|
|
129
|
+
// max_tool_calls, but remain wall-time-bounded and scope/action-checked.
|
|
130
|
+
if (CHARGED_COMMAND_TYPES.has(command.type)) {
|
|
131
|
+
this.#controller.preflightToolCall(phase);
|
|
132
|
+
} else {
|
|
133
|
+
this.#controller.checkWallTime(phase);
|
|
134
|
+
}
|
|
120
135
|
if (command.type === "bash") {
|
|
121
136
|
this.#controller.authorizeBash(command.command);
|
|
122
137
|
return;
|
package/src/sdk.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
Snowflake,
|
|
33
33
|
} from "@gajae-code/utils";
|
|
34
34
|
|
|
35
|
-
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled } from "./async";
|
|
35
|
+
import { type AsyncJob, AsyncJobManager, isBackgroundJobSupportEnabled, jobElapsedMs } from "./async";
|
|
36
36
|
import { loadCapability } from "./capability";
|
|
37
37
|
import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
|
|
38
38
|
import { ModelRegistry } from "./config/model-registry";
|
|
@@ -50,6 +50,7 @@ import { CursorExecHandlers } from "./cursor";
|
|
|
50
50
|
import "./discovery";
|
|
51
51
|
import { resolveConfigValue } from "./config/resolve-config-value";
|
|
52
52
|
import { getEmbeddedDefaultGjcSkills } from "./defaults/gjc-defaults";
|
|
53
|
+
import { BUNDLED_GROK_BUILD_EXTENSION_ID, getBundledGrokBuildExtensionFactory } from "./defaults/gjc-grok-cli";
|
|
53
54
|
import { initializeWithSettings } from "./discovery";
|
|
54
55
|
import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
|
|
55
56
|
import { TtsrManager } from "./export/ttsr";
|
|
@@ -1124,7 +1125,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1124
1125
|
const formattedResult = await formatAsyncResultForFollowUp(result);
|
|
1125
1126
|
if (asyncJobManager!.isDeliverySuppressed(jobId)) return;
|
|
1126
1127
|
|
|
1127
|
-
const durationMs = job ?
|
|
1128
|
+
const durationMs = job ? jobElapsedMs(job) : undefined;
|
|
1128
1129
|
session.yieldQueue.enqueue<AsyncResultEntry>("async-result", {
|
|
1129
1130
|
jobId,
|
|
1130
1131
|
result: formattedResult,
|
|
@@ -1341,13 +1342,26 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1341
1342
|
}
|
|
1342
1343
|
|
|
1343
1344
|
// Extension/module discovery is quarantined; retain only the private
|
|
1344
|
-
// runtime needed for
|
|
1345
|
+
// runtime needed for bundled product extensions, explicitly supplied SDK
|
|
1346
|
+
// extension factories, and custom tools. Filesystem extension paths remain
|
|
1347
|
+
// ignored here even when options.additionalExtensionPaths is supplied.
|
|
1345
1348
|
const extensionsResult: LoadExtensionsResult = options.preloadedExtensions ?? {
|
|
1346
1349
|
extensions: [],
|
|
1347
1350
|
errors: [],
|
|
1348
1351
|
runtime: new ExtensionRuntime(),
|
|
1349
1352
|
};
|
|
1350
1353
|
|
|
1354
|
+
if (!extensionsResult.extensions.some(extension => extension.path === BUNDLED_GROK_BUILD_EXTENSION_ID)) {
|
|
1355
|
+
const bundledGrokExtension = await loadExtensionFromFactory(
|
|
1356
|
+
getBundledGrokBuildExtensionFactory(),
|
|
1357
|
+
cwd,
|
|
1358
|
+
eventBus,
|
|
1359
|
+
extensionsResult.runtime,
|
|
1360
|
+
BUNDLED_GROK_BUILD_EXTENSION_ID,
|
|
1361
|
+
);
|
|
1362
|
+
extensionsResult.extensions.push(bundledGrokExtension);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1351
1365
|
// Load inline extensions from factories
|
|
1352
1366
|
if (inlineExtensions.length > 0) {
|
|
1353
1367
|
for (let i = 0; i < inlineExtensions.length; i++) {
|
|
@@ -322,7 +322,10 @@ function isUnderProjectGjc(cwd: string, targetPath: string): boolean {
|
|
|
322
322
|
|
|
323
323
|
/** Listener function for agent session events */
|
|
324
324
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
325
|
-
export type AsyncJobSnapshotItem = Pick<
|
|
325
|
+
export type AsyncJobSnapshotItem = Pick<
|
|
326
|
+
AsyncJob,
|
|
327
|
+
"id" | "type" | "status" | "label" | "startTime" | "endTime" | "metadata"
|
|
328
|
+
>;
|
|
326
329
|
|
|
327
330
|
export interface AsyncJobSnapshot {
|
|
328
331
|
running: AsyncJobSnapshotItem[];
|
|
@@ -903,6 +906,7 @@ export class AgentSession {
|
|
|
903
906
|
// Compaction state
|
|
904
907
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
905
908
|
#autoCompactionAbortController: AbortController | undefined = undefined;
|
|
909
|
+
#prePromptContextCheckPromise: Promise<void> | undefined = undefined;
|
|
906
910
|
|
|
907
911
|
// Branch summarization state
|
|
908
912
|
#branchSummaryAbortController: AbortController | undefined = undefined;
|
|
@@ -1563,6 +1567,7 @@ export class AgentSession {
|
|
|
1563
1567
|
status: job.status,
|
|
1564
1568
|
label: job.label,
|
|
1565
1569
|
startTime: job.startTime,
|
|
1570
|
+
endTime: job.endTime,
|
|
1566
1571
|
metadata: job.metadata,
|
|
1567
1572
|
}));
|
|
1568
1573
|
const recent = manager.getRecentJobs(options?.recentLimit ?? 5, ownerFilter).map(job => ({
|
|
@@ -1571,6 +1576,7 @@ export class AgentSession {
|
|
|
1571
1576
|
status: job.status,
|
|
1572
1577
|
label: job.label,
|
|
1573
1578
|
startTime: job.startTime,
|
|
1579
|
+
endTime: job.endTime,
|
|
1574
1580
|
metadata: job.metadata,
|
|
1575
1581
|
}));
|
|
1576
1582
|
const delivery = manager.getDeliveryState(ownerFilter);
|
|
@@ -4754,7 +4760,11 @@ export class AgentSession {
|
|
|
4754
4760
|
await this.#checkCompaction(lastAssistant, false);
|
|
4755
4761
|
}
|
|
4756
4762
|
if (!options?.skipCompactionCheck) {
|
|
4757
|
-
await this.#checkEstimatedContextBeforePrompt(
|
|
4763
|
+
await this.#checkEstimatedContextBeforePrompt([
|
|
4764
|
+
...(options?.prependMessages ?? []),
|
|
4765
|
+
message,
|
|
4766
|
+
...this.#pendingNextTurnMessages,
|
|
4767
|
+
]);
|
|
4758
4768
|
}
|
|
4759
4769
|
|
|
4760
4770
|
// Build messages array (session context, eager todo prelude, then active prompt message)
|
|
@@ -5219,7 +5229,9 @@ export class AgentSession {
|
|
|
5219
5229
|
}
|
|
5220
5230
|
await this.#syncSkillPromptActiveStateSafely(appMessage, true);
|
|
5221
5231
|
try {
|
|
5222
|
-
await this
|
|
5232
|
+
await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
5233
|
+
skipPostPromptRecoveryWait: true,
|
|
5234
|
+
});
|
|
5223
5235
|
} finally {
|
|
5224
5236
|
await this.#syncSkillPromptActiveStateSafely(appMessage, false);
|
|
5225
5237
|
}
|
|
@@ -5243,7 +5255,9 @@ export class AgentSession {
|
|
|
5243
5255
|
}
|
|
5244
5256
|
await this.#syncSkillPromptActiveStateSafely(appMessage, true);
|
|
5245
5257
|
try {
|
|
5246
|
-
await this
|
|
5258
|
+
await this.#promptWithMessage(appMessage, this.#getCustomMessageTextContent(appMessage), {
|
|
5259
|
+
skipPostPromptRecoveryWait: true,
|
|
5260
|
+
});
|
|
5247
5261
|
} finally {
|
|
5248
5262
|
await this.#syncSkillPromptActiveStateSafely(appMessage, false);
|
|
5249
5263
|
}
|
|
@@ -6546,7 +6560,23 @@ export class AgentSession {
|
|
|
6546
6560
|
}
|
|
6547
6561
|
}
|
|
6548
6562
|
|
|
6549
|
-
async #checkEstimatedContextBeforePrompt(): Promise<void> {
|
|
6563
|
+
async #checkEstimatedContextBeforePrompt(pendingMessages: readonly AgentMessage[] = []): Promise<void> {
|
|
6564
|
+
if (this.#prePromptContextCheckPromise) {
|
|
6565
|
+
await this.#prePromptContextCheckPromise;
|
|
6566
|
+
}
|
|
6567
|
+
|
|
6568
|
+
const checkPromise = this.#checkEstimatedContextBeforePromptOnce(pendingMessages);
|
|
6569
|
+
this.#prePromptContextCheckPromise = checkPromise;
|
|
6570
|
+
try {
|
|
6571
|
+
await checkPromise;
|
|
6572
|
+
} finally {
|
|
6573
|
+
if (this.#prePromptContextCheckPromise === checkPromise) {
|
|
6574
|
+
this.#prePromptContextCheckPromise = undefined;
|
|
6575
|
+
}
|
|
6576
|
+
}
|
|
6577
|
+
}
|
|
6578
|
+
|
|
6579
|
+
async #checkEstimatedContextBeforePromptOnce(pendingMessages: readonly AgentMessage[]): Promise<void> {
|
|
6550
6580
|
const model = this.model;
|
|
6551
6581
|
if (!model) return;
|
|
6552
6582
|
const contextWindow = model.contextWindow ?? 0;
|
|
@@ -6554,7 +6584,7 @@ export class AgentSession {
|
|
|
6554
6584
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
6555
6585
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return;
|
|
6556
6586
|
|
|
6557
|
-
let contextTokens = this.#
|
|
6587
|
+
let contextTokens = this.#estimateContextTokensForCompaction(pendingMessages).tokens;
|
|
6558
6588
|
const maxOutputTokens = model.maxTokens ?? 0;
|
|
6559
6589
|
if (!shouldCompact(contextTokens, contextWindow, compactionSettings, maxOutputTokens)) return;
|
|
6560
6590
|
|
|
@@ -9597,6 +9627,21 @@ export class AgentSession {
|
|
|
9597
9627
|
*/
|
|
9598
9628
|
#estimateContextTokens(): {
|
|
9599
9629
|
tokens: number;
|
|
9630
|
+
} {
|
|
9631
|
+
return this.#estimateContextTokensWith(message => this.#estimateMessageDisplayTokens(message));
|
|
9632
|
+
}
|
|
9633
|
+
|
|
9634
|
+
#estimateContextTokensForCompaction(pendingMessages: readonly AgentMessage[]): {
|
|
9635
|
+
tokens: number;
|
|
9636
|
+
} {
|
|
9637
|
+
const estimate = this.#estimateContextTokensWith(message => this.#estimateMessageNativeContextTokens(message));
|
|
9638
|
+
return {
|
|
9639
|
+
tokens: estimate.tokens + this.#estimateMessagesNativeContextTokens(pendingMessages),
|
|
9640
|
+
};
|
|
9641
|
+
}
|
|
9642
|
+
|
|
9643
|
+
#estimateContextTokensWith(estimateMessage: (message: AgentMessage) => number): {
|
|
9644
|
+
tokens: number;
|
|
9600
9645
|
} {
|
|
9601
9646
|
const messages = this.messages;
|
|
9602
9647
|
|
|
@@ -9619,7 +9664,7 @@ export class AgentSession {
|
|
|
9619
9664
|
// No usage data - estimate all messages
|
|
9620
9665
|
let estimated = 0;
|
|
9621
9666
|
for (const message of messages) {
|
|
9622
|
-
estimated +=
|
|
9667
|
+
estimated += estimateMessage(message);
|
|
9623
9668
|
}
|
|
9624
9669
|
return {
|
|
9625
9670
|
tokens: estimated,
|
|
@@ -9629,7 +9674,7 @@ export class AgentSession {
|
|
|
9629
9674
|
const usageTokens = calculatePromptTokens(lastUsage);
|
|
9630
9675
|
let trailingTokens = 0;
|
|
9631
9676
|
for (let i = lastUsageIndex + 1; i < messages.length; i++) {
|
|
9632
|
-
trailingTokens +=
|
|
9677
|
+
trailingTokens += estimateMessage(messages[i]);
|
|
9633
9678
|
}
|
|
9634
9679
|
|
|
9635
9680
|
return {
|
|
@@ -9637,6 +9682,30 @@ export class AgentSession {
|
|
|
9637
9682
|
};
|
|
9638
9683
|
}
|
|
9639
9684
|
|
|
9685
|
+
#estimateMessagesNativeContextTokens(messages: readonly AgentMessage[]): number {
|
|
9686
|
+
let tokens = 0;
|
|
9687
|
+
for (const message of messages) {
|
|
9688
|
+
tokens += this.#estimateMessageNativeContextTokens(message);
|
|
9689
|
+
}
|
|
9690
|
+
return tokens;
|
|
9691
|
+
}
|
|
9692
|
+
|
|
9693
|
+
#estimateMessageDisplayTokens(message: AgentMessage): number {
|
|
9694
|
+
let tokens = 0;
|
|
9695
|
+
for (const llmMessage of convertToLlm([message])) {
|
|
9696
|
+
tokens += estimateMessageTokensHeuristic(llmMessage);
|
|
9697
|
+
}
|
|
9698
|
+
return tokens;
|
|
9699
|
+
}
|
|
9700
|
+
|
|
9701
|
+
#estimateMessageNativeContextTokens(message: AgentMessage): number {
|
|
9702
|
+
let tokens = 0;
|
|
9703
|
+
for (const llmMessage of convertToLlm([message])) {
|
|
9704
|
+
tokens += estimateTokens(llmMessage);
|
|
9705
|
+
}
|
|
9706
|
+
return tokens;
|
|
9707
|
+
}
|
|
9708
|
+
|
|
9640
9709
|
/**
|
|
9641
9710
|
* Export session to HTML.
|
|
9642
9711
|
* @param outputPath Optional output path (defaults to session directory)
|
|
@@ -267,7 +267,13 @@ export function externalizeImageDataSync(blobStore: BlobStore, base64Data: strin
|
|
|
267
267
|
/**
|
|
268
268
|
* Resolve an externalized provider image data URL back to its original string.
|
|
269
269
|
* If the data is not a blob reference, returns it unchanged.
|
|
270
|
-
*
|
|
270
|
+
*
|
|
271
|
+
* LEGACY PERSISTED-IMAGE COMPATIBILITY BOUNDARY: when the persisted blob is missing
|
|
272
|
+
* (e.g. resuming an old session whose image blob was pruned), this warns and returns
|
|
273
|
+
* the reference as-is rather than throwing, so legacy resume degrades gracefully.
|
|
274
|
+
* New resident byte-sensitive TEXT uses the fail-closed path instead
|
|
275
|
+
* (`resolveTextBlobSync` -> `ResidentBlobMissingError`). Do NOT route new byte-sensitive
|
|
276
|
+
* resident data through this warn-and-return path.
|
|
271
277
|
*/
|
|
272
278
|
export async function resolveImageDataUrl(blobStore: BlobStore, data: string): Promise<string> {
|
|
273
279
|
const hash = parseBlobRef(data);
|
|
@@ -284,7 +290,11 @@ export async function resolveImageDataUrl(blobStore: BlobStore, data: string): P
|
|
|
284
290
|
/**
|
|
285
291
|
* Resolve a blob reference back to base64 data.
|
|
286
292
|
* If the data is not a blob reference, returns it unchanged.
|
|
287
|
-
*
|
|
293
|
+
*
|
|
294
|
+
* LEGACY PERSISTED-IMAGE COMPATIBILITY BOUNDARY: when the blob is missing this warns
|
|
295
|
+
* and returns the reference as-is (downstream sees an invalid base64 ref but does not
|
|
296
|
+
* crash), preserving legacy-session resume. Byte-sensitive resident TEXT is fail-closed
|
|
297
|
+
* via `resolveTextBlobSync`; do NOT route new byte-sensitive resident data here.
|
|
288
298
|
*/
|
|
289
299
|
export async function resolveImageData(blobStore: BlobStore, data: string): Promise<string> {
|
|
290
300
|
const hash = parseBlobRef(data);
|
|
@@ -322,7 +332,14 @@ export function resolveImageDataSync(blobStore: BlobStore, data: string): string
|
|
|
322
332
|
return buffer.toString("base64");
|
|
323
333
|
}
|
|
324
334
|
|
|
325
|
-
/**
|
|
335
|
+
/**
|
|
336
|
+
* Synchronously resolve a blob reference back to utf8 text.
|
|
337
|
+
*
|
|
338
|
+
* FAIL-CLOSED byte-sensitive path: a missing resident blob throws
|
|
339
|
+
* `ResidentBlobMissingError` rather than degrading, so a missing resident text blob can
|
|
340
|
+
* never silently leak a `blob:sha256:` ref into provider payloads, UI, or exports.
|
|
341
|
+
* (Contrast the legacy persisted-image warn-and-return resolvers above.)
|
|
342
|
+
*/
|
|
326
343
|
export function resolveTextBlobSync(
|
|
327
344
|
blobStore: BlobStore,
|
|
328
345
|
data: string,
|
|
@@ -336,3 +353,42 @@ export function resolveTextBlobSync(
|
|
|
336
353
|
}
|
|
337
354
|
return buffer.toString("utf8");
|
|
338
355
|
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* FAIL-CLOSED resident variant of {@link resolveImageDataUrlSync}: a missing resident
|
|
359
|
+
* image-data-url blob throws `ResidentBlobMissingError` ("imageUrl") instead of warn-returning,
|
|
360
|
+
* so resident byte-sensitive provider image data can never leak a `blob:sha256:` ref into
|
|
361
|
+
* materialized entries, context, or provider payloads. The warn-and-return `resolveImageDataUrl*`
|
|
362
|
+
* resolvers remain ONLY for legacy persisted-image resume.
|
|
363
|
+
*/
|
|
364
|
+
export function resolveResidentImageDataUrlSync(
|
|
365
|
+
blobStore: BlobStore,
|
|
366
|
+
data: string,
|
|
367
|
+
context?: { sessionId?: string; sessionFile?: string },
|
|
368
|
+
): string {
|
|
369
|
+
const hash = parseBlobRef(data);
|
|
370
|
+
if (!hash) return data;
|
|
371
|
+
const buffer = blobStore.getSync(hash);
|
|
372
|
+
if (!buffer) {
|
|
373
|
+
throw new ResidentBlobMissingError(hash, "imageUrl", context?.sessionId, context?.sessionFile);
|
|
374
|
+
}
|
|
375
|
+
return buffer.toString("utf8");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* FAIL-CLOSED resident variant of {@link resolveImageDataSync}: a missing resident image blob
|
|
380
|
+
* throws `ResidentBlobMissingError` ("imageData") instead of warn-returning a placeholder.
|
|
381
|
+
*/
|
|
382
|
+
export function resolveResidentImageDataSync(
|
|
383
|
+
blobStore: BlobStore,
|
|
384
|
+
data: string,
|
|
385
|
+
context?: { sessionId?: string; sessionFile?: string },
|
|
386
|
+
): string {
|
|
387
|
+
const hash = parseBlobRef(data);
|
|
388
|
+
if (!hash) return data;
|
|
389
|
+
const buffer = blobStore.getSync(hash);
|
|
390
|
+
if (!buffer) {
|
|
391
|
+
throw new ResidentBlobMissingError(hash, "imageData", context?.sessionId, context?.sessionFile);
|
|
392
|
+
}
|
|
393
|
+
return buffer.toString("base64");
|
|
394
|
+
}
|
|
@@ -41,9 +41,9 @@ import {
|
|
|
41
41
|
isImageDataUrl,
|
|
42
42
|
MemoryBlobStore,
|
|
43
43
|
resolveImageData,
|
|
44
|
-
resolveImageDataSync,
|
|
45
44
|
resolveImageDataUrl,
|
|
46
|
-
|
|
45
|
+
resolveResidentImageDataSync,
|
|
46
|
+
resolveResidentImageDataUrlSync,
|
|
47
47
|
resolveTextBlobSync,
|
|
48
48
|
} from "./blob-store";
|
|
49
49
|
import {
|
|
@@ -1265,9 +1265,9 @@ function materializeResidentValueSync(
|
|
|
1265
1265
|
if (cached !== undefined) return cached;
|
|
1266
1266
|
const resolved =
|
|
1267
1267
|
obj.kind === "imageUrl"
|
|
1268
|
-
?
|
|
1268
|
+
? resolveResidentImageDataUrlSync(stores.imageStore, obj.ref, stores)
|
|
1269
1269
|
: obj.kind === "imageData"
|
|
1270
|
-
?
|
|
1270
|
+
? resolveResidentImageDataSync(stores.imageStore, obj.ref, stores)
|
|
1271
1271
|
: resolveTextBlobSync(stores.textStore, obj.ref, stores);
|
|
1272
1272
|
cache.set(cacheKey, resolved);
|
|
1273
1273
|
return resolved;
|
|
@@ -9,14 +9,20 @@ These instructions teach a Hermes-style coordinator how to operate GJC through t
|
|
|
9
9
|
1. Use `{{TOOL_PREFIX}}_list_sessions` to find an existing session, or `{{TOOL_PREFIX}}_start_session` when a new session is required and mutation is enabled.
|
|
10
10
|
2. Send exactly one bounded task prompt with `{{TOOL_PREFIX}}_send_prompt`.
|
|
11
11
|
3. Store the returned `turn_id`.
|
|
12
|
-
4.
|
|
12
|
+
4. Prefer `{{TOOL_PREFIX}}_watch_events` with the stored `latest_seq` for event-driven progress; fall back to `{{TOOL_PREFIX}}_read_turn` or `{{TOOL_PREFIX}}_await_turn` for a specific `turn_id` until terminal.
|
|
13
13
|
If a second task is needed while one turn is active, pass `queue: true`; the next queued turn is promoted after the active turn is reported terminal.
|
|
14
14
|
5. If GJC asks a structured question, use `{{TOOL_PREFIX}}_list_questions` and answer with `{{TOOL_PREFIX}}_submit_question_answer`.
|
|
15
15
|
6. Use `{{TOOL_PREFIX}}_report_status` for coordinator-visible status and final reports.
|
|
16
16
|
7. Use `{{TOOL_PREFIX}}_read_tail` only as advisory debug output when structured turn state is insufficient.
|
|
17
17
|
|
|
18
|
+
## Event watch
|
|
19
|
+
|
|
20
|
+
`{{TOOL_PREFIX}}_watch_events` is a bounded long-poll read tool. Call it with `after_seq` set to the last stored sequence number, optional `session_id` or `event_types`, `timeout_ms` up to 30000, and `limit` up to 100. Store the returned `latest_seq` before the next wait. A timeout with no events is not failure; call again or use the turn/status read tools for a snapshot.
|
|
21
|
+
|
|
18
22
|
Do not report completion to the user until the GJC turn is terminal. Do not infer completion from terminal scrollback alone.
|
|
19
23
|
|
|
24
|
+
Coordinator MCP is a durable polling/await bridge, not a push subscription stream. Use `{{TOOL_PREFIX}}_read_coordination_status`, `{{TOOL_PREFIX}}_read_turn`, and bounded `{{TOOL_PREFIX}}_await_turn` as the authoritative consumption surface.
|
|
25
|
+
|
|
20
26
|
## Worktree, model, and provider policy
|
|
21
27
|
|
|
22
28
|
The Hermes bridge does not choose a model/provider. Generated setup configures `GJC_COORDINATOR_MCP_SESSION_COMMAND` to `gjc --worktree` by default, so GJC creates and tracks the worktree while still using normal local model/provider resolution. Keep worktree creation inside GJC rather than creating unmanaged Hermes-side git worktrees; this preserves the original project identity for session listing and resume. If the operator config supplies a different `GJC_COORDINATOR_MCP_SESSION_COMMAND`, preserve it as explicit user intent.
|