@gajae-code/coding-agent 0.3.2 → 0.4.0
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 +32 -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 +9 -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/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 +57 -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/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
package/src/lsp/client.ts
CHANGED
|
@@ -61,6 +61,10 @@ function stopIdleChecker(): void {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export function isIdleCheckerActiveForTests(): boolean {
|
|
65
|
+
return idleCheckInterval !== null;
|
|
66
|
+
}
|
|
67
|
+
|
|
64
68
|
// =============================================================================
|
|
65
69
|
// Client Capabilities
|
|
66
70
|
// =============================================================================
|
|
@@ -919,6 +923,9 @@ export async function sendNotification(client: LspClient, method: string, params
|
|
|
919
923
|
* Shutdown all LSP clients.
|
|
920
924
|
*/
|
|
921
925
|
export async function shutdownAll(): Promise<void> {
|
|
926
|
+
stopIdleChecker();
|
|
927
|
+
clientLocks.clear();
|
|
928
|
+
fileOperationLocks.clear();
|
|
922
929
|
const clientsToShutdown = Array.from(clients.values());
|
|
923
930
|
clients.clear();
|
|
924
931
|
await Promise.allSettled(clientsToShutdown.map(client => shutdownClientInstance(client)));
|
|
@@ -50,6 +50,7 @@ import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../
|
|
|
50
50
|
import type { ExtensionUIContext, ExtensionUIDialogOptions } from "../../extensibility/extensions";
|
|
51
51
|
import { runExtensionCompact } from "../../extensibility/extensions/compact-handler";
|
|
52
52
|
import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
|
|
53
|
+
import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
|
|
53
54
|
import {
|
|
54
55
|
buildSkillPromptMessage,
|
|
55
56
|
getSkillSlashCommandNames,
|
|
@@ -722,7 +723,18 @@ export class AcpAgent implements Agent {
|
|
|
722
723
|
for (let index = 0; index < invocations.length; index += 1) {
|
|
723
724
|
const invocation = invocations[index];
|
|
724
725
|
if (!invocation) continue;
|
|
725
|
-
const
|
|
726
|
+
const activationResult = await resolveSubskillActivationForSkillInvocation({
|
|
727
|
+
cwd: record.session.sessionManager.getCwd(),
|
|
728
|
+
sessionId: record.session.sessionId,
|
|
729
|
+
skillName: invocation.skill.name,
|
|
730
|
+
args: invocation.args,
|
|
731
|
+
});
|
|
732
|
+
const built = await buildSkillPromptMessage(invocation.skill, activationResult.cleanedArgs, {
|
|
733
|
+
subskillActivation: activationResult.activation,
|
|
734
|
+
subskillActivationSet: activationResult.activeSubskillsToPersist,
|
|
735
|
+
cwd: record.session.sessionManager.getCwd(),
|
|
736
|
+
sessionId: record.session.sessionId,
|
|
737
|
+
});
|
|
726
738
|
if (index === invocations.length - 1) {
|
|
727
739
|
await record.session.promptCustomMessage({
|
|
728
740
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
@@ -757,7 +769,18 @@ export class AcpAgent implements Agent {
|
|
|
757
769
|
if (!skill || skill.hide === true) {
|
|
758
770
|
return false;
|
|
759
771
|
}
|
|
760
|
-
const
|
|
772
|
+
const activationResult = await resolveSubskillActivationForSkillInvocation({
|
|
773
|
+
cwd: record.session.sessionManager.getCwd(),
|
|
774
|
+
sessionId: record.session.sessionId,
|
|
775
|
+
skillName: skill.name,
|
|
776
|
+
args,
|
|
777
|
+
});
|
|
778
|
+
const built = await buildSkillPromptMessage(skill, activationResult.cleanedArgs, {
|
|
779
|
+
subskillActivation: activationResult.activation,
|
|
780
|
+
subskillActivationSet: activationResult.activeSubskillsToPersist,
|
|
781
|
+
cwd: record.session.sessionManager.getCwd(),
|
|
782
|
+
sessionId: record.session.sessionId,
|
|
783
|
+
});
|
|
761
784
|
await record.session.promptCustomMessage({
|
|
762
785
|
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
763
786
|
content: built.message,
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
1
2
|
import type { ExtensionUIContext } from "../../extensibility/extensions";
|
|
2
3
|
import type { AgentSession } from "../../session/agent-session";
|
|
3
4
|
import type { ClientBridgePermissionOutcome } from "../../session/client-bridge";
|
|
4
|
-
import type { RpcCommand, RpcResponse } from "../rpc/rpc-types";
|
|
5
|
+
import type { RpcCommand, RpcResponse, RpcWorkflowGateResponse } from "../rpc/rpc-types";
|
|
5
6
|
import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
|
|
6
7
|
import { isRpcCommand } from "../shared/agent-wire/command-validation";
|
|
7
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
BridgeFrameSequencer,
|
|
10
|
+
toBridgeEventFrame,
|
|
11
|
+
toBridgeWorkflowGateFrame,
|
|
12
|
+
} from "../shared/agent-wire/event-envelope";
|
|
8
13
|
import type { BridgeCapability } from "../shared/agent-wire/handshake";
|
|
9
14
|
import {
|
|
10
15
|
type BridgeHandshakeRequest,
|
|
@@ -23,6 +28,9 @@ import {
|
|
|
23
28
|
} from "../shared/agent-wire/scopes";
|
|
24
29
|
import { UiRequestBroker } from "../shared/agent-wire/ui-request-broker";
|
|
25
30
|
import type { BridgeUiResult } from "../shared/agent-wire/ui-result";
|
|
31
|
+
import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
|
|
32
|
+
import { UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
|
|
33
|
+
import { FileGateStore } from "../shared/agent-wire/workflow-gate-broker";
|
|
26
34
|
import { assertSafeBridgeBind, isBridgeTokenAuthorized } from "./auth";
|
|
27
35
|
import { type BridgePermissionRequestPayload, createBridgeClientBridge } from "./bridge-client-bridge";
|
|
28
36
|
import { BridgeExtensionUIContext, type BridgeUiRequestPayload } from "./bridge-ui-context";
|
|
@@ -39,6 +47,7 @@ const SERVER_CAPABILITIES: readonly BridgeCapability[] = [
|
|
|
39
47
|
"ui.declarative",
|
|
40
48
|
"host_tools",
|
|
41
49
|
"host_uri",
|
|
50
|
+
"workflow_gate",
|
|
42
51
|
];
|
|
43
52
|
|
|
44
53
|
const DEFAULT_BRIDGE_SCOPES: readonly BridgeCommandScope[] = ["prompt"];
|
|
@@ -71,6 +80,7 @@ const SERVER_FRAME_TYPES: readonly BridgeFrameType[] = [
|
|
|
71
80
|
"host_tool_call",
|
|
72
81
|
"host_uri_request",
|
|
73
82
|
"reset",
|
|
83
|
+
"workflow_gate",
|
|
74
84
|
"error",
|
|
75
85
|
];
|
|
76
86
|
|
|
@@ -86,6 +96,7 @@ interface BridgeFetchHandlerOptions {
|
|
|
86
96
|
hostToolBridge?: RpcHostToolBridge;
|
|
87
97
|
hostUriBridge?: RpcHostUriBridge;
|
|
88
98
|
endpointMatrix?: Partial<BridgeEndpointMatrix>;
|
|
99
|
+
unattendedControlPlane?: UnattendedSessionControlPlane;
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
interface BridgeIdempotencyRecord {
|
|
@@ -174,6 +185,15 @@ function bridgeHelpResponse(matrix: BridgeEndpointMatrix): Response {
|
|
|
174
185
|
});
|
|
175
186
|
}
|
|
176
187
|
|
|
188
|
+
function auditOutcomeFor(event: string): "accepted" | "rejected" | "denied" | "exceeded" | "aborted" | "info" {
|
|
189
|
+
if (event.includes("denied")) return "denied";
|
|
190
|
+
if (event.includes("exceeded")) return "exceeded";
|
|
191
|
+
if (event.includes("abort")) return "aborted";
|
|
192
|
+
if (event.includes("rejected") || event.includes("conflict")) return "rejected";
|
|
193
|
+
if (event.includes("accepted") || event.includes("negotiated") || event.includes("emitted")) return "accepted";
|
|
194
|
+
return "info";
|
|
195
|
+
}
|
|
196
|
+
|
|
177
197
|
function frameTypeForDispatchOutput(obj: RpcResponse | object): BridgeFrameType {
|
|
178
198
|
const type = typeof obj === "object" && obj !== null && "type" in obj ? (obj as { type?: unknown }).type : undefined;
|
|
179
199
|
if (type === "host_tool_call" || type === "host_tool_cancel") return "host_tool_call";
|
|
@@ -219,6 +239,24 @@ export function createBridgeFetchHandler(options: BridgeFetchHandlerOptions): (r
|
|
|
219
239
|
if (!isBridgeHandshakeRequest(payload)) {
|
|
220
240
|
return jsonResponse(400, { error: "invalid_request" });
|
|
221
241
|
}
|
|
242
|
+
let acceptedUnattended = options.unattendedControlPlane?.isUnattended() ? payload.unattended : undefined;
|
|
243
|
+
if (
|
|
244
|
+
acceptedUnattended === undefined &&
|
|
245
|
+
payload.unattended !== undefined &&
|
|
246
|
+
endpointMatrix.events &&
|
|
247
|
+
options.unattendedControlPlane
|
|
248
|
+
) {
|
|
249
|
+
try {
|
|
250
|
+
options.unattendedControlPlane.negotiate(payload.unattended);
|
|
251
|
+
acceptedUnattended = payload.unattended;
|
|
252
|
+
} catch (err) {
|
|
253
|
+
const error =
|
|
254
|
+
err instanceof Error && "code" in err
|
|
255
|
+
? { code: (err as { code: unknown }).code, message: err.message }
|
|
256
|
+
: { error: err instanceof Error ? err.message : String(err) };
|
|
257
|
+
return jsonResponse(403, error);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
222
260
|
return jsonResponse(
|
|
223
261
|
200,
|
|
224
262
|
negotiateBridgeHandshake(payload, {
|
|
@@ -243,6 +281,7 @@ export function createBridgeFetchHandler(options: BridgeFetchHandlerOptions): (r
|
|
|
243
281
|
: "",
|
|
244
282
|
},
|
|
245
283
|
frameTypes: endpointMatrix.events ? SERVER_FRAME_TYPES : [],
|
|
284
|
+
acceptedUnattended,
|
|
246
285
|
}),
|
|
247
286
|
);
|
|
248
287
|
}
|
|
@@ -360,6 +399,36 @@ export function createBridgeFetchHandler(options: BridgeFetchHandlerOptions): (r
|
|
|
360
399
|
} catch {
|
|
361
400
|
return jsonResponse(400, { error: "invalid_json" });
|
|
362
401
|
}
|
|
402
|
+
if (
|
|
403
|
+
payload !== null &&
|
|
404
|
+
typeof payload === "object" &&
|
|
405
|
+
"gate_id" in payload &&
|
|
406
|
+
"answer" in payload &&
|
|
407
|
+
(correlationId === (payload as RpcWorkflowGateResponse).gate_id || correlationId.startsWith("wg_"))
|
|
408
|
+
) {
|
|
409
|
+
try {
|
|
410
|
+
const resolution = await options.unattendedControlPlane?.resolveGate({
|
|
411
|
+
gate_id: (payload as RpcWorkflowGateResponse).gate_id,
|
|
412
|
+
answer: (payload as RpcWorkflowGateResponse).answer,
|
|
413
|
+
idempotency_key: (payload as RpcWorkflowGateResponse).idempotency_key ?? idempotencyKey,
|
|
414
|
+
});
|
|
415
|
+
if (resolution) {
|
|
416
|
+
rememberIdempotencyResponse(options.idempotencyCache, idempotencyKey, {
|
|
417
|
+
route: url.pathname,
|
|
418
|
+
ownerToken,
|
|
419
|
+
body,
|
|
420
|
+
response: resolution,
|
|
421
|
+
});
|
|
422
|
+
return jsonResponse(200, resolution);
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
const error =
|
|
426
|
+
err instanceof Error && "code" in err
|
|
427
|
+
? { code: (err as { code: unknown }).code, message: err.message }
|
|
428
|
+
: { error: err instanceof Error ? err.message : String(err) };
|
|
429
|
+
return jsonResponse(409, error);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
363
432
|
const permissionResult = options.permissionBroker?.respond(
|
|
364
433
|
correlationId,
|
|
365
434
|
ownerToken,
|
|
@@ -490,6 +559,57 @@ export async function runBridgeMode(
|
|
|
490
559
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
491
560
|
const hostUriBridge = new RpcHostUriBridge(output);
|
|
492
561
|
const idempotencyCache: BridgeIdempotencyCache = new Map();
|
|
562
|
+
const auditLog = new UnattendedAuditLog(defaultAuditPath(session.sessionId, session.sessionManager.getCwd()), {
|
|
563
|
+
redactAnswers: true,
|
|
564
|
+
});
|
|
565
|
+
const recordAudit = (event: { event: string; [key: string]: unknown }) => {
|
|
566
|
+
const payload =
|
|
567
|
+
typeof event.payload === "object" && event.payload !== null
|
|
568
|
+
? (event.payload as Record<string, unknown>)
|
|
569
|
+
: undefined;
|
|
570
|
+
const gateId =
|
|
571
|
+
typeof event.gate_id === "string"
|
|
572
|
+
? event.gate_id
|
|
573
|
+
: typeof payload?.gate_id === "string"
|
|
574
|
+
? payload.gate_id
|
|
575
|
+
: undefined;
|
|
576
|
+
auditLog.record({
|
|
577
|
+
run_id: session.sessionId,
|
|
578
|
+
session_id: session.sessionId,
|
|
579
|
+
actor: typeof event.actor === "string" ? event.actor : undefined,
|
|
580
|
+
event: event.event,
|
|
581
|
+
outcome: auditOutcomeFor(event.event),
|
|
582
|
+
dedupe_key: `${event.event}:${gateId ?? "run"}:${JSON.stringify(payload ?? event)}`,
|
|
583
|
+
gate_id: gateId,
|
|
584
|
+
stage: typeof event.stage === "string" ? (event.stage as never) : undefined,
|
|
585
|
+
kind: typeof event.kind === "string" ? (event.kind as never) : undefined,
|
|
586
|
+
scope: typeof payload?.scope === "string" ? payload.scope : undefined,
|
|
587
|
+
action: typeof payload?.action === "string" ? payload.action : undefined,
|
|
588
|
+
budget: event.event === "budget_exceeded" ? (payload as never) : undefined,
|
|
589
|
+
answer_hash: typeof event.answer_hash === "string" ? event.answer_hash : undefined,
|
|
590
|
+
error: payload && event.event.endsWith("denied") ? payload : undefined,
|
|
591
|
+
});
|
|
592
|
+
};
|
|
593
|
+
const gateStore = new FileGateStore(
|
|
594
|
+
path.join(session.sessionManager.getCwd(), ".gjc", "state", "workflow-gates", `${session.sessionId}.json`),
|
|
595
|
+
);
|
|
596
|
+
const unattendedControlPlane = new UnattendedSessionControlPlane({
|
|
597
|
+
runId: session.sessionId,
|
|
598
|
+
sessionId: session.sessionId,
|
|
599
|
+
emitFrame: gate => eventStream.publish(toBridgeWorkflowGateFrame(gate, sequencer)),
|
|
600
|
+
store: gateStore,
|
|
601
|
+
audit: recordAudit,
|
|
602
|
+
getUsageSnapshot: () => {
|
|
603
|
+
const stats = session.getSessionStats();
|
|
604
|
+
return { tokens: stats.tokens.total, costUsd: stats.cost };
|
|
605
|
+
},
|
|
606
|
+
});
|
|
607
|
+
session.setWorkflowGateEmitter(unattendedControlPlane);
|
|
608
|
+
unattendedControlPlane
|
|
609
|
+
.recover()
|
|
610
|
+
.catch(err =>
|
|
611
|
+
eventStream.publish(sequencer.next("error", { error: err instanceof Error ? err.message : String(err) })),
|
|
612
|
+
);
|
|
493
613
|
|
|
494
614
|
Bun.serve({
|
|
495
615
|
hostname,
|
|
@@ -505,6 +625,7 @@ export async function runBridgeMode(
|
|
|
505
625
|
hostToolBridge,
|
|
506
626
|
hostUriBridge,
|
|
507
627
|
commandScopes,
|
|
628
|
+
unattendedControlPlane,
|
|
508
629
|
commandDispatcher: command =>
|
|
509
630
|
dispatchRpcCommand(command, {
|
|
510
631
|
session,
|
|
@@ -512,6 +633,7 @@ export async function runBridgeMode(
|
|
|
512
633
|
hostToolRegistry: hostToolBridge,
|
|
513
634
|
hostUriRegistry: hostUriBridge,
|
|
514
635
|
createUiContext: () => uiContext,
|
|
636
|
+
unattendedControlPlane,
|
|
515
637
|
}),
|
|
516
638
|
}),
|
|
517
639
|
});
|
|
@@ -4,6 +4,7 @@ import { type AgentMessage, ThinkingLevel } from "@gajae-code/agent-core";
|
|
|
4
4
|
import type { AutocompleteProvider, SlashCommand } from "@gajae-code/tui";
|
|
5
5
|
import { $env, sanitizeText } from "@gajae-code/utils";
|
|
6
6
|
import { isSettingsInitialized, settings } from "../../config/settings";
|
|
7
|
+
import { resolveSubskillActivationForSkillInvocation } from "../../extensibility/gjc-plugins";
|
|
7
8
|
import { buildSkillPromptMessage, parseSkillInvocations } from "../../extensibility/skills";
|
|
8
9
|
import { expandEmoticons } from "../../modes/emoji-autocomplete";
|
|
9
10
|
import { createPromptActionAutocompleteProvider } from "../../modes/prompt-action-autocomplete";
|
|
@@ -472,9 +473,20 @@ export class InputController {
|
|
|
472
473
|
for (let index = 0; index < invocations.length; index += 1) {
|
|
473
474
|
const invocation = invocations[index];
|
|
474
475
|
if (!invocation) continue;
|
|
475
|
-
const
|
|
476
|
+
const activationResult = await resolveSubskillActivationForSkillInvocation({
|
|
477
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
478
|
+
sessionId: this.ctx.session.sessionId,
|
|
479
|
+
skillName: invocation.skill.name,
|
|
480
|
+
args: invocation.args,
|
|
481
|
+
});
|
|
482
|
+
const built = await buildSkillPromptMessage(invocation.skill, activationResult.cleanedArgs, {
|
|
483
|
+
subskillActivation: activationResult.activation,
|
|
484
|
+
subskillActivationSet: activationResult.activeSubskillsToPersist,
|
|
485
|
+
cwd: this.ctx.sessionManager.getCwd(),
|
|
486
|
+
sessionId: this.ctx.session.sessionId,
|
|
487
|
+
});
|
|
476
488
|
const details: SkillPromptDetails = built.details;
|
|
477
|
-
const displayText = `/${invocation.commandName}${
|
|
489
|
+
const displayText = `/${invocation.commandName}${activationResult.cleanedArgs ? ` ${activationResult.cleanedArgs}` : ""}`;
|
|
478
490
|
// When the agent is streaming, register a compact slash-form text as
|
|
479
491
|
// the pending-display twin BEFORE dispatching the CustomMessage. The
|
|
480
492
|
// returned tag is embedded in details so AgentSession.#handleAgentEvent
|
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
getKeybindings,
|
|
6
|
-
type SlashCommand,
|
|
7
|
-
} from "@gajae-code/tui";
|
|
8
|
-
import { formatKeyHints, type KeybindingsManager } from "../config/keybindings";
|
|
1
|
+
import type { AutocompleteItem, AutocompleteProvider, SlashCommand } from "@gajae-code/tui";
|
|
2
|
+
import { CombinedAutocompleteProvider, getKeybindings, getSlashCommandMatchRank } from "@gajae-code/tui";
|
|
3
|
+
import type { KeybindingsManager } from "../config/keybindings";
|
|
4
|
+
import { formatKeyHints } from "../config/keybindings";
|
|
9
5
|
import { isSettingsInitialized, settings } from "../config/settings";
|
|
10
6
|
import { applyEmojiCompletion, getEmojiSuggestions, isEmojiPrefix, tryEmojiInlineReplace } from "./emoji-autocomplete";
|
|
11
7
|
|
|
@@ -104,6 +100,43 @@ function mergeAutocompleteSuggestions(
|
|
|
104
100
|
return { items, prefix: primary.prefix };
|
|
105
101
|
}
|
|
106
102
|
|
|
103
|
+
function sortSlashCommandSuggestions(
|
|
104
|
+
suggestions: { items: AutocompleteItem[]; prefix: string } | null,
|
|
105
|
+
commands: SlashCommand[],
|
|
106
|
+
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
107
|
+
if (!suggestions) return null;
|
|
108
|
+
const query = suggestions.prefix.slice(1).toLowerCase();
|
|
109
|
+
const commandIndexes = new Map(commands.map((command, index) => [command.name, index]));
|
|
110
|
+
const commandByName = new Map(commands.map(command => [command.name, command]));
|
|
111
|
+
const items = suggestions.items
|
|
112
|
+
.map((item, index) => {
|
|
113
|
+
const command = commandByName.get(item.value);
|
|
114
|
+
const commandIndex = commandIndexes.get(item.value) ?? index;
|
|
115
|
+
const lowerName = item.value.toLowerCase();
|
|
116
|
+
const lowerDesc = command?.description?.toLowerCase() ?? item.description?.toLowerCase() ?? "";
|
|
117
|
+
const nameScore = fuzzyMatch(query, lowerName) ? fuzzyScore(query, lowerName) : 0;
|
|
118
|
+
const descScore = fuzzyMatch(query, lowerDesc) ? fuzzyScore(query, lowerDesc) * 0.5 : 0;
|
|
119
|
+
return {
|
|
120
|
+
item,
|
|
121
|
+
index,
|
|
122
|
+
commandIndex,
|
|
123
|
+
matchRank: getSlashCommandMatchRank(query, lowerName),
|
|
124
|
+
priority: command?.priority ?? 0,
|
|
125
|
+
score: Math.max(nameScore, descScore),
|
|
126
|
+
};
|
|
127
|
+
})
|
|
128
|
+
.sort(
|
|
129
|
+
(a, b) =>
|
|
130
|
+
a.matchRank - b.matchRank ||
|
|
131
|
+
b.priority - a.priority ||
|
|
132
|
+
b.score - a.score ||
|
|
133
|
+
a.commandIndex - b.commandIndex ||
|
|
134
|
+
a.index - b.index,
|
|
135
|
+
)
|
|
136
|
+
.map(({ item }) => item);
|
|
137
|
+
return { ...suggestions, items };
|
|
138
|
+
}
|
|
139
|
+
|
|
107
140
|
function withoutSkillCommandSuggestions(
|
|
108
141
|
suggestions: { items: AutocompleteItem[]; prefix: string } | null,
|
|
109
142
|
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
@@ -182,7 +215,10 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
182
215
|
await this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol),
|
|
183
216
|
);
|
|
184
217
|
const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor);
|
|
185
|
-
return
|
|
218
|
+
return sortSlashCommandSuggestions(
|
|
219
|
+
mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions),
|
|
220
|
+
this.#commands,
|
|
221
|
+
);
|
|
186
222
|
}
|
|
187
223
|
|
|
188
224
|
if (!isSettingsInitialized() || settings.get("emojiAutocomplete")) {
|
|
@@ -253,7 +289,10 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
253
289
|
this.#baseProvider.trySyncSlashCompletion?.(textBeforeCursor) ?? null,
|
|
254
290
|
);
|
|
255
291
|
const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor);
|
|
256
|
-
return
|
|
292
|
+
return sortSlashCommandSuggestions(
|
|
293
|
+
mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions),
|
|
294
|
+
this.#commands,
|
|
295
|
+
);
|
|
257
296
|
}
|
|
258
297
|
trySyncInlineReplace(textBeforeCursor: string): { replaceLen: number; insert: string } | null {
|
|
259
298
|
if (isSettingsInitialized() && !settings.get("emojiAutocomplete")) return null;
|
|
@@ -20,6 +20,9 @@ import type {
|
|
|
20
20
|
RpcHostToolUpdate,
|
|
21
21
|
RpcResponse,
|
|
22
22
|
RpcSessionState,
|
|
23
|
+
RpcWorkflowGate,
|
|
24
|
+
RpcWorkflowGateResolution,
|
|
25
|
+
RpcWorkflowGateResponse,
|
|
23
26
|
} from "./rpc-types";
|
|
24
27
|
|
|
25
28
|
/** Distributive Omit that works with union types */
|
|
@@ -97,7 +100,7 @@ function isRpcResponse(value: unknown): value is RpcResponse {
|
|
|
97
100
|
if (typeof value.success !== "boolean") return false;
|
|
98
101
|
if (value.id !== undefined && typeof value.id !== "string") return false;
|
|
99
102
|
if (value.success === false) {
|
|
100
|
-
return typeof value.error === "string";
|
|
103
|
+
return typeof value.error === "string" || isRecord(value.error);
|
|
101
104
|
}
|
|
102
105
|
return true;
|
|
103
106
|
}
|
|
@@ -130,6 +133,21 @@ function isRpcExtensionUiRequest(value: unknown): value is RpcExtensionUIRequest
|
|
|
130
133
|
return value.type === "extension_ui_request" && typeof value.id === "string" && typeof value.method === "string";
|
|
131
134
|
}
|
|
132
135
|
|
|
136
|
+
function isRpcWorkflowGate(value: unknown): value is RpcWorkflowGate {
|
|
137
|
+
if (!isRecord(value)) return false;
|
|
138
|
+
return (
|
|
139
|
+
value.type === "workflow_gate" &&
|
|
140
|
+
typeof value.gate_id === "string" &&
|
|
141
|
+
typeof value.stage === "string" &&
|
|
142
|
+
typeof value.kind === "string" &&
|
|
143
|
+
isRecord(value.schema) &&
|
|
144
|
+
typeof value.schema_hash === "string" &&
|
|
145
|
+
isRecord(value.context) &&
|
|
146
|
+
typeof value.created_at === "string" &&
|
|
147
|
+
value.required === true
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
133
151
|
function normalizeToolResult<TDetails>(result: RpcClientToolResult<TDetails>): AgentToolResult<TDetails> {
|
|
134
152
|
if (typeof result === "string") {
|
|
135
153
|
return {
|
|
@@ -152,6 +170,7 @@ export class RpcClient {
|
|
|
152
170
|
#pendingHostToolCalls = new Map<string, { controller: AbortController }>();
|
|
153
171
|
#requestId = 0;
|
|
154
172
|
#extensionUiListeners: Set<(req: RpcExtensionUIRequest) => void> = new Set();
|
|
173
|
+
#workflowGateListeners: Set<(gate: RpcWorkflowGate) => void> = new Set();
|
|
155
174
|
#abortController = new AbortController();
|
|
156
175
|
|
|
157
176
|
constructor(private options: RpcClientOptions = {}) {
|
|
@@ -299,6 +318,29 @@ export class RpcClient {
|
|
|
299
318
|
};
|
|
300
319
|
}
|
|
301
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Subscribe to workflow lifecycle gates emitted by RPC mode.
|
|
323
|
+
*/
|
|
324
|
+
onWorkflowGate(listener: (gate: RpcWorkflowGate) => void): () => void {
|
|
325
|
+
this.#workflowGateListeners.add(listener);
|
|
326
|
+
return () => {
|
|
327
|
+
this.#workflowGateListeners.delete(listener);
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Answer a workflow lifecycle gate and wait for the server resolution envelope.
|
|
333
|
+
*/
|
|
334
|
+
async respondGate(gateId: string, answer: unknown, idempotencyKey?: string): Promise<RpcWorkflowGateResolution> {
|
|
335
|
+
const response = await this.#send({
|
|
336
|
+
type: "workflow_gate_response",
|
|
337
|
+
gate_id: gateId,
|
|
338
|
+
answer,
|
|
339
|
+
idempotency_key: idempotencyKey,
|
|
340
|
+
});
|
|
341
|
+
return this.#getData(response);
|
|
342
|
+
}
|
|
343
|
+
|
|
302
344
|
/**
|
|
303
345
|
* Get collected stderr output (useful for debugging).
|
|
304
346
|
*/
|
|
@@ -687,6 +729,13 @@ export class RpcClient {
|
|
|
687
729
|
return;
|
|
688
730
|
}
|
|
689
731
|
|
|
732
|
+
if (isRpcWorkflowGate(data)) {
|
|
733
|
+
for (const listener of this.#workflowGateListeners) {
|
|
734
|
+
listener(data);
|
|
735
|
+
}
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
690
739
|
if (isRpcHostToolCancelRequest(data)) {
|
|
691
740
|
this.#pendingHostToolCalls.get(data.targetId)?.controller.abort();
|
|
692
741
|
return;
|
|
@@ -798,7 +847,10 @@ export class RpcClient {
|
|
|
798
847
|
}
|
|
799
848
|
}
|
|
800
849
|
|
|
801
|
-
#writeFrame(
|
|
850
|
+
#writeFrame(
|
|
851
|
+
frame: RpcCommand | RpcWorkflowGateResponse | RpcHostToolResult | RpcHostToolUpdate,
|
|
852
|
+
onError?: (error: Error) => void,
|
|
853
|
+
): void {
|
|
802
854
|
if (!this.#process?.stdin) {
|
|
803
855
|
throw new Error("Client not started");
|
|
804
856
|
}
|
|
@@ -815,7 +867,9 @@ export class RpcClient {
|
|
|
815
867
|
#getData<T>(response: RpcResponse): T {
|
|
816
868
|
if (!response.success) {
|
|
817
869
|
const errorResponse = response as Extract<RpcResponse, { success: false }>;
|
|
818
|
-
throw new Error(
|
|
870
|
+
throw new Error(
|
|
871
|
+
typeof errorResponse.error === "string" ? errorResponse.error : JSON.stringify(errorResponse.error),
|
|
872
|
+
);
|
|
819
873
|
}
|
|
820
874
|
// Type assertion: we trust response.data matches T based on the command sent.
|
|
821
875
|
// This is safe because each public method specifies the correct T for its command.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* - Events: AgentSessionEvent objects streamed as they occur
|
|
11
11
|
* - Extension UI: Extension UI requests are emitted, client responds with extension_ui_response
|
|
12
12
|
*/
|
|
13
|
+
import * as path from "node:path";
|
|
13
14
|
import { $env, readJsonl, Snowflake } from "@gajae-code/utils";
|
|
14
15
|
import type {
|
|
15
16
|
ExtensionUIContext,
|
|
@@ -21,6 +22,9 @@ import type { AgentSession } from "../../session/agent-session";
|
|
|
21
22
|
import { initializeExtensions } from "../runtime-init";
|
|
22
23
|
import { dispatchRpcCommand } from "../shared/agent-wire/command-dispatch";
|
|
23
24
|
import { rpcError as error } from "../shared/agent-wire/responses";
|
|
25
|
+
import { defaultAuditPath, UnattendedAuditLog } from "../shared/agent-wire/unattended-audit";
|
|
26
|
+
import { UnattendedSessionControlPlane } from "../shared/agent-wire/unattended-session";
|
|
27
|
+
import { FileGateStore } from "../shared/agent-wire/workflow-gate-broker";
|
|
24
28
|
import { isRpcHostToolResult, isRpcHostToolUpdate, RpcHostToolBridge } from "./host-tools";
|
|
25
29
|
import { isRpcHostUriResult, RpcHostUriBridge } from "./host-uris";
|
|
26
30
|
import type {
|
|
@@ -72,6 +76,15 @@ function shouldEmitRpcTitles(): boolean {
|
|
|
72
76
|
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
73
77
|
}
|
|
74
78
|
|
|
79
|
+
function auditOutcomeFor(event: string): "accepted" | "rejected" | "denied" | "exceeded" | "aborted" | "info" {
|
|
80
|
+
if (event.includes("denied")) return "denied";
|
|
81
|
+
if (event.includes("exceeded")) return "exceeded";
|
|
82
|
+
if (event.includes("abort")) return "aborted";
|
|
83
|
+
if (event.includes("rejected") || event.includes("conflict")) return "rejected";
|
|
84
|
+
if (event.includes("accepted") || event.includes("negotiated") || event.includes("emitted")) return "accepted";
|
|
85
|
+
return "info";
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
export function requestRpcEditor(
|
|
76
89
|
pendingRequests: Map<string, PendingExtensionRequest>,
|
|
77
90
|
output: RpcOutput,
|
|
@@ -159,6 +172,59 @@ export async function runRpcMode(
|
|
|
159
172
|
const pendingExtensionRequests = new Map<string, PendingExtensionRequest>();
|
|
160
173
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
161
174
|
const hostUriBridge = new RpcHostUriBridge(output);
|
|
175
|
+
const auditLog = new UnattendedAuditLog(defaultAuditPath(session.sessionId, session.sessionManager.getCwd()), {
|
|
176
|
+
redactAnswers: true,
|
|
177
|
+
});
|
|
178
|
+
const recordAudit = (event: { event: string; [key: string]: unknown }) => {
|
|
179
|
+
const payload =
|
|
180
|
+
typeof event.payload === "object" && event.payload !== null
|
|
181
|
+
? (event.payload as Record<string, unknown>)
|
|
182
|
+
: undefined;
|
|
183
|
+
const gateId =
|
|
184
|
+
typeof event.gate_id === "string"
|
|
185
|
+
? event.gate_id
|
|
186
|
+
: typeof payload?.gate_id === "string"
|
|
187
|
+
? payload.gate_id
|
|
188
|
+
: undefined;
|
|
189
|
+
auditLog.record({
|
|
190
|
+
run_id: session.sessionId,
|
|
191
|
+
session_id: session.sessionId,
|
|
192
|
+
actor: typeof event.actor === "string" ? event.actor : undefined,
|
|
193
|
+
event: event.event,
|
|
194
|
+
outcome: auditOutcomeFor(event.event),
|
|
195
|
+
dedupe_key: `${event.event}:${gateId ?? "run"}:${JSON.stringify(payload ?? event)}`,
|
|
196
|
+
gate_id: gateId,
|
|
197
|
+
stage: typeof event.stage === "string" ? (event.stage as never) : undefined,
|
|
198
|
+
kind: typeof event.kind === "string" ? (event.kind as never) : undefined,
|
|
199
|
+
scope: typeof payload?.scope === "string" ? payload.scope : undefined,
|
|
200
|
+
action: typeof payload?.action === "string" ? payload.action : undefined,
|
|
201
|
+
budget: event.event === "budget_exceeded" ? (payload as never) : undefined,
|
|
202
|
+
answer_hash: typeof event.answer_hash === "string" ? event.answer_hash : undefined,
|
|
203
|
+
error: payload && event.event.endsWith("denied") ? payload : undefined,
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
// Unattended control plane (#318/#319/#323/G011): routes negotiate_unattended +
|
|
207
|
+
// workflow_gate_response and lets skill runtimes emit gates over RPC.
|
|
208
|
+
const gateStore = new FileGateStore(
|
|
209
|
+
path.join(session.sessionManager.getCwd(), ".gjc", "state", "workflow-gates", `${session.sessionId}.json`),
|
|
210
|
+
);
|
|
211
|
+
const unattendedControlPlane = new UnattendedSessionControlPlane({
|
|
212
|
+
runId: session.sessionId,
|
|
213
|
+
sessionId: session.sessionId,
|
|
214
|
+
emitFrame: gate => output(gate),
|
|
215
|
+
store: gateStore,
|
|
216
|
+
audit: recordAudit,
|
|
217
|
+
getUsageSnapshot: () => {
|
|
218
|
+
const stats = session.getSessionStats();
|
|
219
|
+
return { tokens: stats.tokens.total, costUsd: stats.cost };
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
unattendedControlPlane
|
|
223
|
+
.recover()
|
|
224
|
+
.catch(err =>
|
|
225
|
+
output(error(undefined, "workflow_gate_recover", err instanceof Error ? err.message : String(err))),
|
|
226
|
+
);
|
|
227
|
+
session.setWorkflowGateEmitter(unattendedControlPlane);
|
|
162
228
|
|
|
163
229
|
// Shutdown request flag (wrapped in object to allow mutation with const)
|
|
164
230
|
const shutdownState = { requested: false };
|
|
@@ -419,6 +485,7 @@ export async function runRpcMode(
|
|
|
419
485
|
hostToolRegistry: hostToolBridge,
|
|
420
486
|
hostUriRegistry: hostUriBridge,
|
|
421
487
|
createUiContext: () => new RpcExtensionUIContext(pendingExtensionRequests, output),
|
|
488
|
+
unattendedControlPlane,
|
|
422
489
|
});
|
|
423
490
|
|
|
424
491
|
/**
|