@gajae-code/coding-agent 0.3.2 → 0.4.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 +39 -0
- package/dist/types/config/model-registry.d.ts +17 -10
- package/dist/types/config/models-config-schema.d.ts +37 -0
- package/dist/types/config/settings-schema.d.ts +5 -0
- package/dist/types/edit/diff.d.ts +16 -0
- package/dist/types/edit/modes/replace.d.ts +7 -0
- package/dist/types/extensibility/gjc-plugins/activation.d.ts +14 -0
- package/dist/types/extensibility/gjc-plugins/index.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/injection.d.ts +31 -0
- package/dist/types/extensibility/gjc-plugins/loader.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/schema.d.ts +3 -0
- package/dist/types/extensibility/gjc-plugins/state.d.ts +9 -0
- package/dist/types/extensibility/gjc-plugins/tools.d.ts +8 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/validation.d.ts +4 -0
- package/dist/types/extensibility/skills.d.ts +9 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +22 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -2
- package/dist/types/harness-control-plane/storage.d.ts +7 -0
- package/dist/types/lsp/client.d.ts +1 -0
- package/dist/types/modes/bridge/bridge-mode.d.ts +2 -0
- package/dist/types/modes/prompt-action-autocomplete.d.ts +2 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +19 -1
- package/dist/types/modes/rpc/rpc-types.d.ts +179 -2
- package/dist/types/modes/shared/agent-wire/approval-gate.d.ts +57 -0
- package/dist/types/modes/shared/agent-wire/command-dispatch.d.ts +16 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +47 -0
- package/dist/types/modes/shared/agent-wire/event-envelope.d.ts +7 -0
- package/dist/types/modes/shared/agent-wire/handshake.d.ts +11 -1
- package/dist/types/modes/shared/agent-wire/protocol.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/responses.d.ts +1 -1
- package/dist/types/modes/shared/agent-wire/unattended-action-policy.d.ts +27 -0
- package/dist/types/modes/shared/agent-wire/unattended-audit.d.ts +68 -0
- package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +161 -0
- package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +61 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +114 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-schema.d.ts +39 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +0 -4
- package/dist/types/sdk.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/blob-store.d.ts +17 -0
- package/dist/types/session/messages.d.ts +3 -0
- package/dist/types/session/session-storage.d.ts +6 -0
- package/dist/types/skill-state/active-state.d.ts +13 -0
- package/dist/types/thinking.d.ts +3 -2
- package/dist/types/tools/index.d.ts +3 -0
- package/package.json +9 -7
- package/src/cli.ts +14 -0
- package/src/commands/harness.ts +192 -7
- package/src/commands/ultragoal.ts +1 -21
- package/src/config/model-equivalence.ts +1 -1
- package/src/config/model-registry.ts +32 -5
- package/src/config/models-config-schema.ts +7 -2
- package/src/config/settings-schema.ts +4 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +19 -23
- package/src/defaults/gjc/skills/ralplan/SKILL.md +7 -7
- package/src/discovery/claude-plugins.ts +25 -5
- package/src/edit/diff.ts +64 -1
- package/src/edit/modes/replace.ts +60 -2
- package/src/extensibility/gjc-plugins/activation.ts +87 -0
- package/src/extensibility/gjc-plugins/index.ts +9 -0
- package/src/extensibility/gjc-plugins/injection.ts +114 -0
- package/src/extensibility/gjc-plugins/loader.ts +131 -0
- package/src/extensibility/gjc-plugins/paths.ts +66 -0
- package/src/extensibility/gjc-plugins/schema.ts +79 -0
- package/src/extensibility/gjc-plugins/state.ts +29 -0
- package/src/extensibility/gjc-plugins/tools.ts +47 -0
- package/src/extensibility/gjc-plugins/types.ts +97 -0
- package/src/extensibility/gjc-plugins/validation.ts +76 -0
- package/src/extensibility/skills.ts +39 -7
- package/src/gjc-runtime/state-runtime.ts +93 -2
- package/src/gjc-runtime/state-writer.ts +17 -1
- package/src/gjc-runtime/ultragoal-runtime.ts +76 -121
- package/src/gjc-runtime/workflow-manifest.generated.json +5 -0
- package/src/gjc-runtime/workflow-manifest.ts +2 -2
- package/src/harness-control-plane/storage.ts +144 -2
- package/src/hashline/hash.ts +23 -0
- package/src/hooks/skill-state.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +5 -5
- package/src/lsp/client.ts +7 -0
- package/src/modes/acp/acp-agent.ts +25 -2
- package/src/modes/bridge/bridge-mode.ts +124 -2
- package/src/modes/controllers/input-controller.ts +14 -2
- package/src/modes/prompt-action-autocomplete.ts +49 -10
- package/src/modes/rpc/rpc-client.ts +79 -3
- package/src/modes/rpc/rpc-mode.ts +67 -0
- package/src/modes/rpc/rpc-types.ts +224 -2
- package/src/modes/shared/agent-wire/approval-gate.ts +151 -0
- package/src/modes/shared/agent-wire/command-dispatch.ts +97 -4
- package/src/modes/shared/agent-wire/command-validation.ts +25 -1
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +222 -0
- package/src/modes/shared/agent-wire/event-envelope.ts +13 -0
- package/src/modes/shared/agent-wire/handshake.ts +43 -3
- package/src/modes/shared/agent-wire/protocol.ts +7 -0
- package/src/modes/shared/agent-wire/responses.ts +2 -2
- package/src/modes/shared/agent-wire/scopes.ts +2 -0
- package/src/modes/shared/agent-wire/unattended-action-policy.ts +341 -0
- package/src/modes/shared/agent-wire/unattended-audit.ts +175 -0
- package/src/modes/shared/agent-wire/unattended-run-controller.ts +406 -0
- package/src/modes/shared/agent-wire/unattended-session.ts +180 -0
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +324 -0
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +331 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/prompts/system/system-prompt.md +9 -0
- package/src/runtime-mcp/client.ts +7 -4
- package/src/runtime-mcp/manager.ts +45 -13
- package/src/runtime-mcp/transports/http.ts +40 -14
- package/src/runtime-mcp/transports/stdio.ts +11 -10
- package/src/sdk.ts +47 -0
- package/src/session/agent-session.ts +211 -2
- package/src/session/blob-store.ts +84 -0
- package/src/session/messages.ts +3 -0
- package/src/session/session-manager.ts +390 -33
- package/src/session/session-storage.ts +26 -0
- package/src/setup/provider-onboarding.ts +2 -2
- package/src/skill-state/active-state.ts +89 -1
- package/src/task/discovery.ts +7 -1
- package/src/task/executor.ts +16 -2
- package/src/thinking.ts +8 -2
- package/src/tools/ask.ts +39 -9
- package/src/tools/index.ts +3 -0
- package/src/tools/skill.ts +15 -3
- package/src/utils/edit-mode.ts +1 -1
|
@@ -69,6 +69,13 @@ Use for read-only plan critique. It approves only when execution can proceed wit
|
|
|
69
69
|
- Before explicit execution approval, planning workflows NEVER edit product source, run mutation-oriented shell commands, commit, push, open PRs, or delegate implementation tasks.
|
|
70
70
|
</routing>
|
|
71
71
|
|
|
72
|
+
<skill-discipline>
|
|
73
|
+
- Never ignore a skill invocation or any skill text. When a skill is active, read it in full and follow its instructions exactly. Do not assume, paraphrase, reorder, or substitute steps.
|
|
74
|
+
- Read-only and interview-style skills (e.g. `deep-interview`, `planner`, `architect`, `critic`) MUST NOT implement, edit product source, commit, or run mutating commands. Honor each skill's read-only or pending-approval boundary even when the fix looks obvious.
|
|
75
|
+
- When a task fits a bundled skill, recommend invoking the corresponding `/skill:<name>`; on user approval, invoke it. Never silently bypass an applicable skill.
|
|
76
|
+
- When no skill is active, or the active skill explicitly permits the action, and the action is non-destructive and clearly correct, perform it directly instead of asking.
|
|
77
|
+
</skill-discipline>
|
|
78
|
+
|
|
72
79
|
<runtime-state>
|
|
73
80
|
- Runtime state, specs, plans, and workflow ledgers belong under `.gjc/`.
|
|
74
81
|
- Default workflow skills are bundled from `packages/coding-agent/src/defaults/gjc/skills/`. Runtime user/project `.gjc` discovery remains supported, but committed repo-visible `.gjc` defaults are not the source of truth.
|
|
@@ -82,6 +89,8 @@ Use for read-only plan critique. It approves only when execution can proceed wit
|
|
|
82
89
|
- Do not narrate progress, ceremony, timing, scope inflation, or session limits.
|
|
83
90
|
- If the user's intent is clear, act without asking. Ask only when the next step is destructive or requires a missing choice that materially changes the outcome.
|
|
84
91
|
- When the user proposes something wrong, say what breaks and what to do instead once; then defer to their call.
|
|
92
|
+
- Never use permission-begging or deferral phrasing ("if you want", "if you'd like", "shall I", "I will now", "next I plan to"). For a destructive next step, state the recommended action and stop for approval. For a non-destructive, clearly correct next step, do it directly in the same turn.
|
|
93
|
+
- Do not defer actionable work. Underpromise and overdeliver: report only what is done or in progress, never announce remaining work instead of doing it.
|
|
85
94
|
</communication>
|
|
86
95
|
|
|
87
96
|
<completion-contract>
|
|
@@ -141,6 +141,8 @@ export async function connectToServer(
|
|
|
141
141
|
): Promise<MCPServerConnection> {
|
|
142
142
|
const timeoutMs = config.timeout ?? CONNECTION_TIMEOUT_MS;
|
|
143
143
|
let transport: MCPTransport | undefined;
|
|
144
|
+
const connectAbort = new AbortController();
|
|
145
|
+
const connectSignal = options?.signal ? AbortSignal.any([options.signal, connectAbort.signal]) : connectAbort.signal;
|
|
144
146
|
|
|
145
147
|
const connect = async (): Promise<MCPServerConnection> => {
|
|
146
148
|
transport = await createTransport(config);
|
|
@@ -155,7 +157,7 @@ export async function connectToServer(
|
|
|
155
157
|
|
|
156
158
|
try {
|
|
157
159
|
const initResult = await initializeConnection(transport, {
|
|
158
|
-
signal:
|
|
160
|
+
signal: connectSignal,
|
|
159
161
|
async onInitialized() {
|
|
160
162
|
// Open the SSE stream before sending initialized, so server-to-client
|
|
161
163
|
// requests triggered by on_initialized (e.g. roots/list) are delivered.
|
|
@@ -184,13 +186,14 @@ export async function connectToServer(
|
|
|
184
186
|
connect(),
|
|
185
187
|
timeoutMs,
|
|
186
188
|
`Connection to MCP server "${name}" timed out after ${timeoutMs}ms`,
|
|
187
|
-
|
|
189
|
+
connectSignal,
|
|
188
190
|
);
|
|
189
191
|
} catch (error) {
|
|
190
192
|
// If withTimeout rejected (timeout/abort) while connect() was still pending,
|
|
191
|
-
//
|
|
193
|
+
// abort initialization and wait for transport cleanup before returning.
|
|
194
|
+
connectAbort.abort(error);
|
|
192
195
|
if (transport) {
|
|
193
|
-
|
|
196
|
+
await transport.close().catch(() => {});
|
|
194
197
|
}
|
|
195
198
|
throw error;
|
|
196
199
|
}
|
|
@@ -152,6 +152,7 @@ export class MCPManager {
|
|
|
152
152
|
#connections = new Map<string, MCPServerConnection>();
|
|
153
153
|
#tools: CustomTool<TSchema, MCPToolDetails>[] = [];
|
|
154
154
|
#pendingConnections = new Map<string, Promise<MCPServerConnection>>();
|
|
155
|
+
#pendingConnectionControllers = new Map<string, AbortController>();
|
|
155
156
|
#pendingToolLoads = new Map<string, Promise<ToolLoadResult>>();
|
|
156
157
|
#sources = new Map<string, SourceMeta>();
|
|
157
158
|
#authStorage: AuthStorage | null = null;
|
|
@@ -164,6 +165,7 @@ export class MCPManager {
|
|
|
164
165
|
#subscribedResources = new Map<string, Set<string>>();
|
|
165
166
|
#pendingResourceRefresh = new Map<string, { connection: MCPServerConnection; promise: Promise<void> }>();
|
|
166
167
|
#pendingReconnections = new Map<string, Promise<MCPServerConnection | null>>();
|
|
168
|
+
#disconnectEpochs = new Map<string, number>();
|
|
167
169
|
/** Preserved configs for reconnection after connection loss. */
|
|
168
170
|
#serverConfigs = new Map<string, MCPServerConfig>();
|
|
169
171
|
/** Monotonic epoch incremented on disconnectAll to invalidate stale reconnections. */
|
|
@@ -348,10 +350,14 @@ export class MCPManager {
|
|
|
348
350
|
// and falls back to cached/deferred tools.
|
|
349
351
|
this.#serverConfigs.set(name, config);
|
|
350
352
|
|
|
353
|
+
const connectionEpoch = this.#epoch;
|
|
354
|
+
const connectionAbort = new AbortController();
|
|
355
|
+
this.#pendingConnectionControllers.set(name, connectionAbort);
|
|
351
356
|
// Resolve auth config before connecting, but do so per-server in parallel.
|
|
352
357
|
const connectionPromise = (async () => {
|
|
353
358
|
const resolvedConfig = await this.#resolveAuthConfig(config);
|
|
354
359
|
return connectToServer(name, resolvedConfig, {
|
|
360
|
+
signal: connectionAbort.signal,
|
|
355
361
|
onNotification: (method, params) => {
|
|
356
362
|
this.#handleServerNotification(name, method, params);
|
|
357
363
|
},
|
|
@@ -360,18 +366,26 @@ export class MCPManager {
|
|
|
360
366
|
},
|
|
361
367
|
});
|
|
362
368
|
})().then(
|
|
363
|
-
connection => {
|
|
369
|
+
async connection => {
|
|
364
370
|
// Store original config (without resolved tokens) to keep
|
|
365
371
|
// cache keys stable and avoid leaking rotating credentials.
|
|
366
372
|
connection.config = config;
|
|
367
|
-
this.#serverConfigs.set(name, config);
|
|
368
373
|
if (sources[name]) {
|
|
369
374
|
connection._source = sources[name];
|
|
370
375
|
}
|
|
371
|
-
|
|
376
|
+
const stillPending = this.#pendingConnections.get(name) === connectionPromise;
|
|
377
|
+
const stillCurrent = this.#epoch === connectionEpoch && this.#serverConfigs.get(name) === config;
|
|
378
|
+
if (stillPending) {
|
|
372
379
|
this.#pendingConnections.delete(name);
|
|
373
|
-
this.#
|
|
380
|
+
this.#pendingConnectionControllers.delete(name);
|
|
374
381
|
}
|
|
382
|
+
if (!stillPending || !stillCurrent) {
|
|
383
|
+
connection.transport.onClose = undefined;
|
|
384
|
+
await connection.transport.close().catch(() => {});
|
|
385
|
+
throw new Error(`Server "${name}" was disconnected during connection`);
|
|
386
|
+
}
|
|
387
|
+
this.#connections.set(name, connection);
|
|
388
|
+
this.#serverConfigs.set(name, config);
|
|
375
389
|
|
|
376
390
|
// Wire auth refresh for HTTP transports so 401s trigger token refresh.
|
|
377
391
|
if (connection.transport instanceof HttpTransport && config.auth?.type === "oauth") {
|
|
@@ -396,6 +410,7 @@ export class MCPManager {
|
|
|
396
410
|
error => {
|
|
397
411
|
if (this.#pendingConnections.get(name) === connectionPromise) {
|
|
398
412
|
this.#pendingConnections.delete(name);
|
|
413
|
+
this.#pendingConnectionControllers.delete(name);
|
|
399
414
|
}
|
|
400
415
|
throw error;
|
|
401
416
|
},
|
|
@@ -660,13 +675,16 @@ export class MCPManager {
|
|
|
660
675
|
* Disconnect from a specific server.
|
|
661
676
|
*/
|
|
662
677
|
async disconnectServer(name: string): Promise<void> {
|
|
678
|
+
const nextEpoch = (this.#disconnectEpochs.get(name) ?? 0) + 1;
|
|
679
|
+
this.#disconnectEpochs.set(name, nextEpoch);
|
|
680
|
+
this.#pendingConnectionControllers.get(name)?.abort(new Error(`MCP server disconnected: ${name}`));
|
|
681
|
+
this.#pendingConnectionControllers.delete(name);
|
|
663
682
|
this.#pendingConnections.delete(name);
|
|
664
683
|
this.#pendingToolLoads.delete(name);
|
|
665
684
|
this.#pendingReconnections.delete(name);
|
|
666
685
|
this.#sources.delete(name);
|
|
667
686
|
this.#serverConfigs.delete(name);
|
|
668
687
|
this.#pendingResourceRefresh.delete(name);
|
|
669
|
-
|
|
670
688
|
const connection = this.#connections.get(name);
|
|
671
689
|
|
|
672
690
|
const subscribedUris = this.#subscribedResources.get(name);
|
|
@@ -705,6 +723,10 @@ export class MCPManager {
|
|
|
705
723
|
const promises = Array.from(this.#connections.values()).map(conn => disconnectServer(conn));
|
|
706
724
|
await Promise.allSettled(promises);
|
|
707
725
|
|
|
726
|
+
for (const controller of this.#pendingConnectionControllers.values()) {
|
|
727
|
+
controller.abort(new Error("MCP manager disconnected"));
|
|
728
|
+
}
|
|
729
|
+
this.#pendingConnectionControllers.clear();
|
|
708
730
|
this.#pendingConnections.clear();
|
|
709
731
|
this.#pendingToolLoads.clear();
|
|
710
732
|
this.#pendingReconnections.clear();
|
|
@@ -808,14 +830,24 @@ export class MCPManager {
|
|
|
808
830
|
reconnectEpoch: number,
|
|
809
831
|
): Promise<MCPServerConnection> {
|
|
810
832
|
const resolvedConfig = await this.#resolveAuthConfig(config);
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
833
|
+
const connectionAbort = new AbortController();
|
|
834
|
+
this.#pendingConnectionControllers.set(name, connectionAbort);
|
|
835
|
+
let connection: MCPServerConnection;
|
|
836
|
+
try {
|
|
837
|
+
connection = await connectToServer(name, resolvedConfig, {
|
|
838
|
+
signal: connectionAbort.signal,
|
|
839
|
+
onNotification: (method, params) => {
|
|
840
|
+
this.#handleServerNotification(name, method, params);
|
|
841
|
+
},
|
|
842
|
+
onRequest: (method, params) => {
|
|
843
|
+
return this.#handleServerRequest(method, params);
|
|
844
|
+
},
|
|
845
|
+
});
|
|
846
|
+
} finally {
|
|
847
|
+
if (this.#pendingConnectionControllers.get(name) === connectionAbort) {
|
|
848
|
+
this.#pendingConnectionControllers.delete(name);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
819
851
|
|
|
820
852
|
connection.config = config;
|
|
821
853
|
if (source) connection._source = source;
|
|
@@ -25,6 +25,8 @@ export class HttpTransport implements MCPTransport {
|
|
|
25
25
|
#connected = false;
|
|
26
26
|
#sessionId: string | null = null;
|
|
27
27
|
#sseConnection: AbortController | null = null;
|
|
28
|
+
#streamControllers = new Set<AbortController>();
|
|
29
|
+
#streamReaders = new Set<Promise<void>>();
|
|
28
30
|
|
|
29
31
|
onClose?: () => void;
|
|
30
32
|
onError?: (error: Error) => void;
|
|
@@ -52,6 +54,15 @@ export class HttpTransport implements MCPTransport {
|
|
|
52
54
|
this.#connected = true;
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
#trackReader(promise: Promise<void>, controller?: AbortController): void {
|
|
58
|
+
if (controller) this.#streamControllers.add(controller);
|
|
59
|
+
this.#streamReaders.add(promise);
|
|
60
|
+
void promise.finally(() => {
|
|
61
|
+
this.#streamReaders.delete(promise);
|
|
62
|
+
if (controller) this.#streamControllers.delete(controller);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
/**
|
|
56
67
|
* Start SSE listener for server-initiated messages.
|
|
57
68
|
* Resolves once the SSE connection is established (or fails/unsupported).
|
|
@@ -61,7 +72,8 @@ export class HttpTransport implements MCPTransport {
|
|
|
61
72
|
if (!this.#connected) return;
|
|
62
73
|
if (this.#sseConnection) return;
|
|
63
74
|
|
|
64
|
-
|
|
75
|
+
const sseConnection = new AbortController();
|
|
76
|
+
this.#sseConnection = sseConnection;
|
|
65
77
|
const headers: Record<string, string> = {
|
|
66
78
|
Accept: "text/event-stream",
|
|
67
79
|
...this.config.headers,
|
|
@@ -76,10 +88,10 @@ export class HttpTransport implements MCPTransport {
|
|
|
76
88
|
response = await fetch(this.config.url, {
|
|
77
89
|
method: "GET",
|
|
78
90
|
headers,
|
|
79
|
-
signal:
|
|
91
|
+
signal: sseConnection.signal,
|
|
80
92
|
});
|
|
81
93
|
} catch (error) {
|
|
82
|
-
this.#sseConnection = null;
|
|
94
|
+
this.#sseConnection = this.#sseConnection === sseConnection ? null : this.#sseConnection;
|
|
83
95
|
if (error instanceof Error && error.name !== "AbortError") {
|
|
84
96
|
this.onError?.(error);
|
|
85
97
|
}
|
|
@@ -87,19 +99,20 @@ export class HttpTransport implements MCPTransport {
|
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
if (response.status === 405 || !response.ok || !response.body) {
|
|
90
|
-
this.#sseConnection = null;
|
|
102
|
+
this.#sseConnection = this.#sseConnection === sseConnection ? null : this.#sseConnection;
|
|
91
103
|
return;
|
|
92
104
|
}
|
|
93
105
|
|
|
94
106
|
// Connection established — read messages in background.
|
|
95
107
|
// If the stream ends unexpectedly (server restart, network drop),
|
|
96
108
|
// fire onClose so the manager can trigger reconnection.
|
|
97
|
-
const signal =
|
|
98
|
-
|
|
109
|
+
const signal = sseConnection.signal;
|
|
110
|
+
const reader = this.#readSSEStream(response.body!, signal).finally(() => {
|
|
99
111
|
const wasConnected = this.#connected;
|
|
100
|
-
this.#sseConnection = null;
|
|
101
|
-
if (wasConnected) this.onClose?.();
|
|
112
|
+
if (this.#sseConnection === sseConnection) this.#sseConnection = null;
|
|
113
|
+
if (wasConnected && !signal.aborted) this.onClose?.();
|
|
102
114
|
});
|
|
115
|
+
this.#trackReader(reader, sseConnection);
|
|
103
116
|
}
|
|
104
117
|
async #readSSEStream(body: ReadableStream<Uint8Array>, signal: AbortSignal): Promise<void> {
|
|
105
118
|
try {
|
|
@@ -266,6 +279,8 @@ export class HttpTransport implements MCPTransport {
|
|
|
266
279
|
// Re-reading `response.body` after `for await` breaks would lock the
|
|
267
280
|
// stream a second time and surface as "ReadableStream already has a
|
|
268
281
|
// controller", so we must not exit the loop early.
|
|
282
|
+
const drainController = abortController;
|
|
283
|
+
this.#streamControllers.add(drainController);
|
|
269
284
|
const drain = async (): Promise<void> => {
|
|
270
285
|
try {
|
|
271
286
|
for await (const raw of readSseJson<JsonRpcMessage | JsonRpcMessage[]>(response.body!, operationSignal)) {
|
|
@@ -306,10 +321,11 @@ export class HttpTransport implements MCPTransport {
|
|
|
306
321
|
}
|
|
307
322
|
} finally {
|
|
308
323
|
clearTimeout(timeoutId);
|
|
324
|
+
this.#streamControllers.delete(drainController);
|
|
309
325
|
}
|
|
310
326
|
};
|
|
311
327
|
|
|
312
|
-
|
|
328
|
+
this.#trackReader(drain());
|
|
313
329
|
return promise;
|
|
314
330
|
}
|
|
315
331
|
|
|
@@ -417,9 +433,13 @@ export class HttpTransport implements MCPTransport {
|
|
|
417
433
|
// on the notification response (MCP Streamable HTTP spec). Read them.
|
|
418
434
|
const contentType = response.headers.get("Content-Type") ?? "";
|
|
419
435
|
if (contentType.includes("text/event-stream") && response.body) {
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
436
|
+
const streamController = new AbortController();
|
|
437
|
+
const streamTimeout = AbortSignal.timeout(this.config.timeout ?? 30000);
|
|
438
|
+
const signals = this.#sseConnection
|
|
439
|
+
? [this.#sseConnection.signal, streamController.signal, streamTimeout]
|
|
440
|
+
: [streamController.signal, streamTimeout];
|
|
441
|
+
const reader = this.#readSSEStream(response.body, AbortSignal.any(signals));
|
|
442
|
+
this.#trackReader(reader, streamController);
|
|
423
443
|
} else {
|
|
424
444
|
await response.body?.cancel();
|
|
425
445
|
}
|
|
@@ -433,14 +453,20 @@ export class HttpTransport implements MCPTransport {
|
|
|
433
453
|
}
|
|
434
454
|
|
|
435
455
|
async close(): Promise<void> {
|
|
436
|
-
|
|
456
|
+
const wasConnected = this.#connected;
|
|
437
457
|
this.#connected = false;
|
|
438
458
|
|
|
439
|
-
// Abort SSE
|
|
459
|
+
// Abort all SSE/background readers and wait for them to settle.
|
|
460
|
+
for (const controller of this.#streamControllers) {
|
|
461
|
+
controller.abort();
|
|
462
|
+
}
|
|
440
463
|
if (this.#sseConnection) {
|
|
441
464
|
this.#sseConnection.abort();
|
|
442
465
|
this.#sseConnection = null;
|
|
443
466
|
}
|
|
467
|
+
await Promise.allSettled(Array.from(this.#streamReaders));
|
|
468
|
+
|
|
469
|
+
if (!wasConnected && !this.#sessionId) return;
|
|
444
470
|
|
|
445
471
|
// Send session termination if we have a session
|
|
446
472
|
if (this.#sessionId) {
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* Messages are newline-delimited JSON.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { getProjectDir, readJsonl, Snowflake } from "@gajae-code/utils";
|
|
9
|
-
import { type Subprocess, spawn } from "bun";
|
|
8
|
+
import { getProjectDir, ptree, readJsonl, Snowflake } from "@gajae-code/utils";
|
|
10
9
|
import type {
|
|
11
10
|
JsonRpcError,
|
|
12
11
|
JsonRpcMessage,
|
|
@@ -22,8 +21,10 @@ import { toJsonRpcError } from "../../runtime-mcp/types";
|
|
|
22
21
|
* Stdio transport for MCP servers.
|
|
23
22
|
* Spawns a subprocess and communicates via stdin/stdout.
|
|
24
23
|
*/
|
|
24
|
+
const CLOSE_WAIT_MS = 1_000;
|
|
25
|
+
|
|
25
26
|
export class StdioTransport implements MCPTransport {
|
|
26
|
-
#process:
|
|
27
|
+
#process: ptree.ChildProcess<"pipe"> | null = null;
|
|
27
28
|
#pendingRequests = new Map<
|
|
28
29
|
string | number,
|
|
29
30
|
{
|
|
@@ -57,13 +58,11 @@ export class StdioTransport implements MCPTransport {
|
|
|
57
58
|
...this.config.env,
|
|
58
59
|
};
|
|
59
60
|
|
|
60
|
-
this.#process = spawn({
|
|
61
|
-
cmd: [this.config.command, ...args],
|
|
61
|
+
this.#process = ptree.spawn([this.config.command, ...args], {
|
|
62
62
|
cwd: this.config.cwd ?? getProjectDir(),
|
|
63
63
|
env,
|
|
64
64
|
stdin: "pipe",
|
|
65
|
-
|
|
66
|
-
stderr: "pipe",
|
|
65
|
+
stderr: "full",
|
|
67
66
|
});
|
|
68
67
|
|
|
69
68
|
this.#connected = true;
|
|
@@ -299,9 +298,11 @@ export class StdioTransport implements MCPTransport {
|
|
|
299
298
|
}
|
|
300
299
|
this.#pendingRequests.clear();
|
|
301
300
|
|
|
302
|
-
//
|
|
303
|
-
|
|
304
|
-
|
|
301
|
+
// Terminate the subprocess tree and keep the handle until exit is observed.
|
|
302
|
+
const process = this.#process;
|
|
303
|
+
if (process) {
|
|
304
|
+
process.kill();
|
|
305
|
+
await Promise.race([process.exited.catch(() => {}), Bun.sleep(CLOSE_WAIT_MS)]);
|
|
305
306
|
this.#process = null;
|
|
306
307
|
}
|
|
307
308
|
|
package/src/sdk.ts
CHANGED
|
@@ -68,6 +68,8 @@ import {
|
|
|
68
68
|
wrapRegisteredTools,
|
|
69
69
|
} from "./extensibility/extensions";
|
|
70
70
|
import { ExtensionRuntime } from "./extensibility/extensions/loader";
|
|
71
|
+
import { resolveCurrentPhaseForParent } from "./extensibility/gjc-plugins/injection";
|
|
72
|
+
import { loadActiveSubskillTools } from "./extensibility/gjc-plugins/tools";
|
|
71
73
|
import { loadSkills, type Skill, type SkillWarning, setActiveSkills } from "./extensibility/skills";
|
|
72
74
|
import type { FileSlashCommand } from "./extensibility/slash-commands";
|
|
73
75
|
import type { HindsightSessionState } from "./hindsight/state";
|
|
@@ -243,6 +245,8 @@ export interface CreateAgentSessionOptions {
|
|
|
243
245
|
|
|
244
246
|
/** Custom tools to register (in addition to built-in tools). Accepts both CustomTool and ToolDefinition. */
|
|
245
247
|
customTools?: (CustomTool | ToolDefinition)[];
|
|
248
|
+
/** Explicit parent/phase used to load active GJC sub-skill tools for this session. */
|
|
249
|
+
gjcSubskillToolContext?: { parent: string; phase: string; sessionId?: string; cwd?: string };
|
|
246
250
|
/** Inline extensions (merged with discovery). */
|
|
247
251
|
extensions?: ExtensionFactory[];
|
|
248
252
|
/** Additional extension paths to load (merged with discovery). */
|
|
@@ -1183,6 +1187,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1183
1187
|
getActiveModelString,
|
|
1184
1188
|
getPlanModeState: () => session?.getPlanModeState(),
|
|
1185
1189
|
getGoalModeState: () => session?.getGoalModeState(),
|
|
1190
|
+
getWorkflowGateEmitter: () => session?.getWorkflowGateEmitter(),
|
|
1186
1191
|
getGoalRuntime: () => session?.goalRuntime,
|
|
1187
1192
|
getClientBridge: () => session?.clientBridge,
|
|
1188
1193
|
getCompactContext: () => session.formatCompactContext(),
|
|
@@ -1281,6 +1286,47 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1281
1286
|
customTools.push(...getSearchTools());
|
|
1282
1287
|
}
|
|
1283
1288
|
|
|
1289
|
+
const getReservedSubskillToolNames = () => [
|
|
1290
|
+
...new Set([
|
|
1291
|
+
...builtinTools.map(tool => tool.name),
|
|
1292
|
+
...(options.toolNames?.map(name => name.toLowerCase()) ?? []),
|
|
1293
|
+
...(options.customTools?.map(tool => (isCustomTool(tool) ? tool.name : tool.name)) ?? []),
|
|
1294
|
+
...customTools.map(tool => tool.name),
|
|
1295
|
+
]),
|
|
1296
|
+
];
|
|
1297
|
+
|
|
1298
|
+
const gjcSubskillToolContext = options.gjcSubskillToolContext;
|
|
1299
|
+
if (gjcSubskillToolContext?.parent.trim() && gjcSubskillToolContext.phase.trim()) {
|
|
1300
|
+
const pluginTools = await loadActiveSubskillTools({
|
|
1301
|
+
cwd: gjcSubskillToolContext.cwd ?? cwd,
|
|
1302
|
+
sessionId: gjcSubskillToolContext.sessionId ?? logicalSessionId,
|
|
1303
|
+
parent: gjcSubskillToolContext.parent,
|
|
1304
|
+
phase: gjcSubskillToolContext.phase,
|
|
1305
|
+
reservedToolNames: getReservedSubskillToolNames(),
|
|
1306
|
+
});
|
|
1307
|
+
if (pluginTools.length > 0) {
|
|
1308
|
+
customTools.push(...pluginTools);
|
|
1309
|
+
}
|
|
1310
|
+
} else {
|
|
1311
|
+
for (const skill of skills) {
|
|
1312
|
+
const phase = await resolveCurrentPhaseForParent({
|
|
1313
|
+
cwd,
|
|
1314
|
+
sessionId: logicalSessionId,
|
|
1315
|
+
parent: skill.name,
|
|
1316
|
+
});
|
|
1317
|
+
const pluginTools = await loadActiveSubskillTools({
|
|
1318
|
+
cwd,
|
|
1319
|
+
sessionId: logicalSessionId,
|
|
1320
|
+
parent: skill.name,
|
|
1321
|
+
phase,
|
|
1322
|
+
reservedToolNames: getReservedSubskillToolNames(),
|
|
1323
|
+
});
|
|
1324
|
+
if (pluginTools.length > 0) {
|
|
1325
|
+
customTools.push(...pluginTools);
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1284
1330
|
// Custom tool and extension discovery is quarantined from the public GJC utility surface.
|
|
1285
1331
|
// Explicit SDK extension factories are still honored; callers use them to
|
|
1286
1332
|
// register in-process tools/providers without enabling filesystem discovery.
|
|
@@ -1889,6 +1935,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1889
1935
|
modelRegistry,
|
|
1890
1936
|
taskDepth,
|
|
1891
1937
|
toolRegistry,
|
|
1938
|
+
workflowGateToolSession: toolSession,
|
|
1892
1939
|
transformContext,
|
|
1893
1940
|
onPayload,
|
|
1894
1941
|
onResponse,
|