@gajae-code/bridge-client 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.
@@ -0,0 +1,61 @@
1
+ export declare const BRIDGE_CLIENT_COMMAND_TYPES: readonly ["prompt", "steer", "follow_up", "abort", "abort_and_prompt", "new_session", "get_state", "set_todos", "set_host_tools", "set_host_uri_schemes", "workflow_gate_response", "set_model", "cycle_model", "get_available_models", "set_thinking_level", "cycle_thinking_level", "set_steering_mode", "set_follow_up_mode", "set_interrupt_mode", "compact", "set_auto_compaction", "set_auto_retry", "abort_retry", "bash", "abort_bash", "get_session_stats", "export_html", "switch_session", "branch", "get_branch_messages", "get_last_assistant_text", "set_session_name", "handoff", "get_messages", "get_login_providers", "login", "negotiate_unattended"];
2
+ export type BridgeClientCommandType = (typeof BRIDGE_CLIENT_COMMAND_TYPES)[number];
3
+ export type BridgeClientCommand<TType extends BridgeClientCommandType = BridgeClientCommandType> = {
4
+ id?: string;
5
+ type: TType;
6
+ } & Record<string, unknown>;
7
+ export interface BridgeCommandOptions {
8
+ id?: string;
9
+ idempotencyKey?: string;
10
+ }
11
+ export interface BridgeImageCommandOptions extends BridgeCommandOptions {
12
+ images?: unknown[];
13
+ }
14
+ export interface BridgeCommandHelpers {
15
+ prompt(sessionId: string, message: string, options?: BridgeImageCommandOptions & {
16
+ streamingBehavior?: "steer" | "followUp";
17
+ }): Promise<unknown>;
18
+ steer(sessionId: string, message: string, options?: BridgeImageCommandOptions): Promise<unknown>;
19
+ followUp(sessionId: string, message: string, options?: BridgeImageCommandOptions): Promise<unknown>;
20
+ abort(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
21
+ abortAndPrompt(sessionId: string, message: string, options?: BridgeImageCommandOptions): Promise<unknown>;
22
+ newSession(sessionId: string, options?: BridgeCommandOptions & {
23
+ parentSession?: string;
24
+ }): Promise<unknown>;
25
+ getState(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
26
+ setTodos(sessionId: string, phases: unknown[], options?: BridgeCommandOptions): Promise<unknown>;
27
+ setHostTools(sessionId: string, tools: unknown[], options?: BridgeCommandOptions): Promise<unknown>;
28
+ setHostUriSchemes(sessionId: string, schemes: unknown[], options?: BridgeCommandOptions): Promise<unknown>;
29
+ setModel(sessionId: string, provider: string, modelId: string, options?: BridgeCommandOptions): Promise<unknown>;
30
+ cycleModel(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
31
+ getAvailableModels(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
32
+ setThinkingLevel(sessionId: string, level: string, options?: BridgeCommandOptions): Promise<unknown>;
33
+ cycleThinkingLevel(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
34
+ setSteeringMode(sessionId: string, mode: "all" | "one-at-a-time", options?: BridgeCommandOptions): Promise<unknown>;
35
+ setFollowUpMode(sessionId: string, mode: "all" | "one-at-a-time", options?: BridgeCommandOptions): Promise<unknown>;
36
+ setInterruptMode(sessionId: string, mode: "immediate" | "wait", options?: BridgeCommandOptions): Promise<unknown>;
37
+ compact(sessionId: string, options?: BridgeCommandOptions & {
38
+ customInstructions?: string;
39
+ }): Promise<unknown>;
40
+ setAutoCompaction(sessionId: string, enabled: boolean, options?: BridgeCommandOptions): Promise<unknown>;
41
+ setAutoRetry(sessionId: string, enabled: boolean, options?: BridgeCommandOptions): Promise<unknown>;
42
+ abortRetry(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
43
+ bash(sessionId: string, command: string, options?: BridgeCommandOptions): Promise<unknown>;
44
+ abortBash(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
45
+ getSessionStats(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
46
+ exportHtml(sessionId: string, options?: BridgeCommandOptions & {
47
+ outputPath?: string;
48
+ }): Promise<unknown>;
49
+ switchSession(sessionId: string, sessionPath: string, options?: BridgeCommandOptions): Promise<unknown>;
50
+ branch(sessionId: string, entryId: string, options?: BridgeCommandOptions): Promise<unknown>;
51
+ getBranchMessages(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
52
+ getLastAssistantText(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
53
+ setSessionName(sessionId: string, name: string, options?: BridgeCommandOptions): Promise<unknown>;
54
+ handoff(sessionId: string, options?: BridgeCommandOptions & {
55
+ customInstructions?: string;
56
+ }): Promise<unknown>;
57
+ getMessages(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
58
+ getLoginProviders(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
59
+ login(sessionId: string, providerId: string, options?: BridgeCommandOptions): Promise<unknown>;
60
+ respondGate(sessionId: string, gateId: string, ownerToken: string, answer: unknown, options?: BridgeCommandOptions): Promise<unknown>;
61
+ }
@@ -0,0 +1,155 @@
1
+ import type { BridgeClientCommand, BridgeCommandHelpers, BridgeCommandOptions } from "./commands";
2
+ import type { BridgeFrame } from "./reference-consumer";
3
+ export * from "./commands";
4
+ export * from "./reference-consumer";
5
+ export * from "./workflow-gate";
6
+ import type { UnattendedDeclaration, WorkflowGate, WorkflowGateResolver } from "./workflow-gate";
7
+ export type BridgeCapability = "events" | "prompt" | "permission" | "elicitation" | "ui.declarative" | "ui.editor" | "ui.terminal_input" | "host_tools" | "host_uri" | "client_bridge.read_text_file" | "client_bridge.write_text_file" | "client_bridge.create_terminal" | "workflow_gate";
8
+ export type BridgeCommandScope = "prompt" | "control" | "bash" | "export" | "session" | "model" | "message:read" | "host_tools" | "host_uri" | "admin";
9
+ export interface BridgeProtocolRange {
10
+ min: number;
11
+ max: number;
12
+ }
13
+ export interface BridgeHandshakeRequest {
14
+ protocol_version_range: BridgeProtocolRange;
15
+ capabilities: BridgeCapability[];
16
+ requested_scopes: BridgeCommandScope[];
17
+ last_seq?: number;
18
+ unattended?: UnattendedDeclaration;
19
+ }
20
+ export interface BridgeHandshakeAccepted {
21
+ status: "accepted";
22
+ protocol_version: number;
23
+ session_id: string;
24
+ accepted_capabilities: BridgeCapability[];
25
+ accepted_scopes: BridgeCommandScope[];
26
+ unsupported: BridgeCapability[];
27
+ endpoints: {
28
+ events: string;
29
+ commands: string;
30
+ uiResponses: string;
31
+ claimControl: string;
32
+ hostToolResults: string;
33
+ disconnectControl: string;
34
+ hostUriResults: string;
35
+ };
36
+ frame_types: string[];
37
+ accepted_unattended?: UnattendedDeclaration;
38
+ }
39
+ export interface BridgeHandshakeRejected {
40
+ status: "rejected";
41
+ reason: "incompatible_version" | "unauthorized" | "invalid_request";
42
+ message: string;
43
+ }
44
+ export type BridgeFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
45
+ export type BridgeHandshakeResponse = BridgeHandshakeAccepted | BridgeHandshakeRejected;
46
+ export interface BridgeClientOptions {
47
+ baseUrl: string;
48
+ token: string;
49
+ fetch?: BridgeFetch;
50
+ allowInsecureLocalhost?: boolean;
51
+ }
52
+ export declare class BridgeClient implements BridgeCommandHelpers {
53
+ #private;
54
+ constructor(options: BridgeClientOptions);
55
+ handshake(request: BridgeHandshakeRequest): Promise<BridgeHandshakeResponse>;
56
+ command(command: BridgeClientCommand, sessionId: string, idempotencyKey: string): Promise<unknown>;
57
+ prompt(sessionId: string, message: string, options?: {
58
+ id?: string;
59
+ images?: unknown[];
60
+ streamingBehavior?: "steer" | "followUp";
61
+ idempotencyKey?: string;
62
+ }): Promise<unknown>;
63
+ steer(sessionId: string, message: string, options?: {
64
+ id?: string;
65
+ images?: unknown[];
66
+ idempotencyKey?: string;
67
+ }): Promise<unknown>;
68
+ followUp(sessionId: string, message: string, options?: {
69
+ id?: string;
70
+ images?: unknown[];
71
+ idempotencyKey?: string;
72
+ }): Promise<unknown>;
73
+ bash(sessionId: string, command: string, options?: {
74
+ id?: string;
75
+ idempotencyKey?: string;
76
+ }): Promise<unknown>;
77
+ getState(sessionId: string, options?: {
78
+ id?: string;
79
+ idempotencyKey?: string;
80
+ }): Promise<unknown>;
81
+ getMessages(sessionId: string, options?: {
82
+ id?: string;
83
+ idempotencyKey?: string;
84
+ }): Promise<unknown>;
85
+ abort(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
86
+ abortAndPrompt(sessionId: string, message: string, options?: {
87
+ id?: string;
88
+ images?: unknown[];
89
+ idempotencyKey?: string;
90
+ }): Promise<unknown>;
91
+ newSession(sessionId: string, options?: BridgeCommandOptions & {
92
+ parentSession?: string;
93
+ }): Promise<unknown>;
94
+ setTodos(sessionId: string, phases: unknown[], options?: BridgeCommandOptions): Promise<unknown>;
95
+ setHostTools(sessionId: string, tools: unknown[], options?: BridgeCommandOptions): Promise<unknown>;
96
+ setHostUriSchemes(sessionId: string, schemes: unknown[], options?: BridgeCommandOptions): Promise<unknown>;
97
+ setModel(sessionId: string, provider: string, modelId: string, options?: BridgeCommandOptions): Promise<unknown>;
98
+ cycleModel(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
99
+ getAvailableModels(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
100
+ setThinkingLevel(sessionId: string, level: string, options?: BridgeCommandOptions): Promise<unknown>;
101
+ cycleThinkingLevel(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
102
+ setSteeringMode(sessionId: string, mode: "all" | "one-at-a-time", options?: BridgeCommandOptions): Promise<unknown>;
103
+ setFollowUpMode(sessionId: string, mode: "all" | "one-at-a-time", options?: BridgeCommandOptions): Promise<unknown>;
104
+ setInterruptMode(sessionId: string, mode: "immediate" | "wait", options?: BridgeCommandOptions): Promise<unknown>;
105
+ compact(sessionId: string, options?: BridgeCommandOptions & {
106
+ customInstructions?: string;
107
+ }): Promise<unknown>;
108
+ setAutoCompaction(sessionId: string, enabled: boolean, options?: BridgeCommandOptions): Promise<unknown>;
109
+ setAutoRetry(sessionId: string, enabled: boolean, options?: BridgeCommandOptions): Promise<unknown>;
110
+ abortRetry(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
111
+ abortBash(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
112
+ getSessionStats(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
113
+ exportHtml(sessionId: string, options?: BridgeCommandOptions & {
114
+ outputPath?: string;
115
+ }): Promise<unknown>;
116
+ switchSession(sessionId: string, sessionPath: string, options?: BridgeCommandOptions): Promise<unknown>;
117
+ branch(sessionId: string, entryId: string, options?: BridgeCommandOptions): Promise<unknown>;
118
+ getBranchMessages(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
119
+ getLastAssistantText(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
120
+ setSessionName(sessionId: string, name: string, options?: BridgeCommandOptions): Promise<unknown>;
121
+ handoff(sessionId: string, options?: BridgeCommandOptions & {
122
+ customInstructions?: string;
123
+ }): Promise<unknown>;
124
+ getLoginProviders(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
125
+ login(sessionId: string, providerId: string, options?: BridgeCommandOptions): Promise<unknown>;
126
+ createIdempotencyKey(prefix?: string): string;
127
+ events(sessionId: string, lastSeq?: number): AsyncGenerator<BridgeFrame>;
128
+ claimControl(sessionId: string, ownerToken?: string): Promise<unknown>;
129
+ disconnectControl(sessionId: string, ownerToken: string): Promise<unknown>;
130
+ respondToUiRequest(sessionId: string, correlationId: string, ownerToken: string, response: unknown, idempotencyKey?: string): Promise<unknown>;
131
+ /**
132
+ * Answer a `workflow_gate` by posting to the UI-response endpoint and return
133
+ * the gate resolution envelope. Authorization is bearer auth plus the
134
+ * `control` scope; `ownerToken` is carried for idempotency/controller
135
+ * correlation, not as the gate authorization boundary.
136
+ */
137
+ respondGate(sessionId: string, gateId: string, ownerToken: string, answer: unknown, options?: {
138
+ idempotencyKey?: string;
139
+ id?: string;
140
+ }): Promise<unknown>;
141
+ /**
142
+ * Headless policy: stream the session's frames, route every received
143
+ * `workflow_gate` to the agent `resolver`, and post its answer back. Yields
144
+ * each handled gate. The resolver supplies the agent's memory-backed answer.
145
+ */
146
+ consumeWorkflowGates(sessionId: string, ownerToken: string, resolver: WorkflowGateResolver, options?: {
147
+ lastSeq?: number;
148
+ }): AsyncGenerator<{
149
+ gate: WorkflowGate;
150
+ answer: unknown;
151
+ }>;
152
+ respondToHostTool(sessionId: string, correlationId: string, result: unknown): Promise<unknown>;
153
+ respondToHostUri(sessionId: string, correlationId: string, result: unknown): Promise<unknown>;
154
+ connectEvents(sessionId: string, lastSeq?: number): Promise<Response>;
155
+ }
@@ -0,0 +1,20 @@
1
+ export interface BridgeFrame<TPayload = unknown> {
2
+ protocol_version: number;
3
+ session_id: string;
4
+ seq: number;
5
+ frame_id: string;
6
+ correlation_id?: string;
7
+ type: string;
8
+ payload: TPayload;
9
+ }
10
+ export interface RenderedBridgeFrame {
11
+ seq: number;
12
+ type: string;
13
+ html: string;
14
+ }
15
+ export declare function renderBridgeFrame(frame: BridgeFrame): RenderedBridgeFrame;
16
+ export declare class ReferenceBridgeConsumer {
17
+ #private;
18
+ consume(frame: BridgeFrame): RenderedBridgeFrame;
19
+ renderDocument(): string;
20
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Typed `workflow_gate` client helpers (#322).
3
+ *
4
+ * Mirrors the server-side workflow-gate contract for bridge consumers: a typed
5
+ * gate frame, the response shape, a frame type-guard, and a headless policy that
6
+ * routes received gates to an agent callback and posts answers back through the
7
+ * existing owner-token ui-response flow.
8
+ */
9
+ import type { BridgeFrame } from "./reference-consumer";
10
+ export type WorkflowGateStage = "deep-interview" | "ralplan" | "ultragoal";
11
+ export type WorkflowGateKind = "question" | "approval" | "execution";
12
+ export interface WorkflowGateOption {
13
+ value: unknown;
14
+ label: string;
15
+ description?: string;
16
+ }
17
+ export interface WorkflowGate {
18
+ type: "workflow_gate";
19
+ gate_id: string;
20
+ stage: WorkflowGateStage;
21
+ kind: WorkflowGateKind;
22
+ schema: unknown;
23
+ schema_hash: string;
24
+ options?: WorkflowGateOption[];
25
+ context: Record<string, unknown>;
26
+ created_at: string;
27
+ required: true;
28
+ }
29
+ export interface WorkflowGateResponse {
30
+ gate_id: string;
31
+ answer: unknown;
32
+ idempotency_key?: string;
33
+ }
34
+ /** Unattended declaration carried on the bridge handshake (#318/#319). */
35
+ export interface UnattendedDeclaration {
36
+ actor: string;
37
+ budget: {
38
+ max_tokens: number;
39
+ max_tool_calls: number;
40
+ max_wall_time_ms: number;
41
+ max_cost_usd: number;
42
+ };
43
+ scopes: string[];
44
+ action_allowlist: string[];
45
+ }
46
+ /** Type guard: is this bridge frame a fully-formed workflow_gate frame? */
47
+ export declare function isWorkflowGateFrame(frame: BridgeFrame): frame is BridgeFrame<WorkflowGate>;
48
+ /** A callback that produces an answer for a received gate (the agent's "memory"). */
49
+ export type WorkflowGateResolver = (gate: WorkflowGate) => unknown | Promise<unknown>;
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@gajae-code/bridge-client",
4
+ "version": "0.4.0",
5
+ "description": "TypeScript client SDK for the GJC backend bridge protocol",
6
+ "homepage": "https://gaebal-gajae.dev",
7
+ "author": "Yeachan-Heo",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/gajae-ai/gajae-code.git",
12
+ "directory": "packages/bridge-client"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/gajae-ai/gajae-code/issues"
16
+ },
17
+ "keywords": [
18
+ "gjc",
19
+ "bridge",
20
+ "sdk"
21
+ ],
22
+ "main": "./src/index.ts",
23
+ "types": "./dist/types/index.d.ts",
24
+ "scripts": {
25
+ "check": "biome check . && bun run check:types",
26
+ "check:types": "tsgo -p tsconfig.json --noEmit",
27
+ "lint": "biome lint .",
28
+ "test": "bun test",
29
+ "fix": "biome check --write --unsafe .",
30
+ "fmt": "biome format --write ."
31
+ },
32
+ "devDependencies": {
33
+ "@types/bun": "^1.3.14"
34
+ },
35
+ "engines": {
36
+ "bun": ">=1.3.14"
37
+ },
38
+ "files": [
39
+ "src",
40
+ "dist/types"
41
+ ],
42
+ "exports": {
43
+ ".": {
44
+ "types": "./dist/types/index.d.ts",
45
+ "import": "./src/index.ts"
46
+ },
47
+ "./*": {
48
+ "types": "./dist/types/*.d.ts",
49
+ "import": "./src/*.ts"
50
+ }
51
+ }
52
+ }
@@ -0,0 +1,104 @@
1
+ export const BRIDGE_CLIENT_COMMAND_TYPES = [
2
+ "prompt",
3
+ "steer",
4
+ "follow_up",
5
+ "abort",
6
+ "abort_and_prompt",
7
+ "new_session",
8
+ "get_state",
9
+ "set_todos",
10
+ "set_host_tools",
11
+ "set_host_uri_schemes",
12
+ "workflow_gate_response",
13
+ "set_model",
14
+ "cycle_model",
15
+ "get_available_models",
16
+ "set_thinking_level",
17
+ "cycle_thinking_level",
18
+ "set_steering_mode",
19
+ "set_follow_up_mode",
20
+ "set_interrupt_mode",
21
+ "compact",
22
+ "set_auto_compaction",
23
+ "set_auto_retry",
24
+ "abort_retry",
25
+ "bash",
26
+ "abort_bash",
27
+ "get_session_stats",
28
+ "export_html",
29
+ "switch_session",
30
+ "branch",
31
+ "get_branch_messages",
32
+ "get_last_assistant_text",
33
+ "set_session_name",
34
+ "handoff",
35
+ "get_messages",
36
+ "get_login_providers",
37
+ "login",
38
+ "negotiate_unattended",
39
+ ] as const;
40
+
41
+ export type BridgeClientCommandType = (typeof BRIDGE_CLIENT_COMMAND_TYPES)[number];
42
+
43
+ export type BridgeClientCommand<TType extends BridgeClientCommandType = BridgeClientCommandType> = {
44
+ id?: string;
45
+ type: TType;
46
+ } & Record<string, unknown>;
47
+
48
+ export interface BridgeCommandOptions {
49
+ id?: string;
50
+ idempotencyKey?: string;
51
+ }
52
+
53
+ export interface BridgeImageCommandOptions extends BridgeCommandOptions {
54
+ images?: unknown[];
55
+ }
56
+
57
+ export interface BridgeCommandHelpers {
58
+ prompt(
59
+ sessionId: string,
60
+ message: string,
61
+ options?: BridgeImageCommandOptions & { streamingBehavior?: "steer" | "followUp" },
62
+ ): Promise<unknown>;
63
+ steer(sessionId: string, message: string, options?: BridgeImageCommandOptions): Promise<unknown>;
64
+ followUp(sessionId: string, message: string, options?: BridgeImageCommandOptions): Promise<unknown>;
65
+ abort(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
66
+ abortAndPrompt(sessionId: string, message: string, options?: BridgeImageCommandOptions): Promise<unknown>;
67
+ newSession(sessionId: string, options?: BridgeCommandOptions & { parentSession?: string }): Promise<unknown>;
68
+ getState(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
69
+ setTodos(sessionId: string, phases: unknown[], options?: BridgeCommandOptions): Promise<unknown>;
70
+ setHostTools(sessionId: string, tools: unknown[], options?: BridgeCommandOptions): Promise<unknown>;
71
+ setHostUriSchemes(sessionId: string, schemes: unknown[], options?: BridgeCommandOptions): Promise<unknown>;
72
+ setModel(sessionId: string, provider: string, modelId: string, options?: BridgeCommandOptions): Promise<unknown>;
73
+ cycleModel(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
74
+ getAvailableModels(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
75
+ setThinkingLevel(sessionId: string, level: string, options?: BridgeCommandOptions): Promise<unknown>;
76
+ cycleThinkingLevel(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
77
+ setSteeringMode(sessionId: string, mode: "all" | "one-at-a-time", options?: BridgeCommandOptions): Promise<unknown>;
78
+ setFollowUpMode(sessionId: string, mode: "all" | "one-at-a-time", options?: BridgeCommandOptions): Promise<unknown>;
79
+ setInterruptMode(sessionId: string, mode: "immediate" | "wait", options?: BridgeCommandOptions): Promise<unknown>;
80
+ compact(sessionId: string, options?: BridgeCommandOptions & { customInstructions?: string }): Promise<unknown>;
81
+ setAutoCompaction(sessionId: string, enabled: boolean, options?: BridgeCommandOptions): Promise<unknown>;
82
+ setAutoRetry(sessionId: string, enabled: boolean, options?: BridgeCommandOptions): Promise<unknown>;
83
+ abortRetry(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
84
+ bash(sessionId: string, command: string, options?: BridgeCommandOptions): Promise<unknown>;
85
+ abortBash(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
86
+ getSessionStats(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
87
+ exportHtml(sessionId: string, options?: BridgeCommandOptions & { outputPath?: string }): Promise<unknown>;
88
+ switchSession(sessionId: string, sessionPath: string, options?: BridgeCommandOptions): Promise<unknown>;
89
+ branch(sessionId: string, entryId: string, options?: BridgeCommandOptions): Promise<unknown>;
90
+ getBranchMessages(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
91
+ getLastAssistantText(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
92
+ setSessionName(sessionId: string, name: string, options?: BridgeCommandOptions): Promise<unknown>;
93
+ handoff(sessionId: string, options?: BridgeCommandOptions & { customInstructions?: string }): Promise<unknown>;
94
+ getMessages(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
95
+ getLoginProviders(sessionId: string, options?: BridgeCommandOptions): Promise<unknown>;
96
+ login(sessionId: string, providerId: string, options?: BridgeCommandOptions): Promise<unknown>;
97
+ respondGate(
98
+ sessionId: string,
99
+ gateId: string,
100
+ ownerToken: string,
101
+ answer: unknown,
102
+ options?: BridgeCommandOptions,
103
+ ): Promise<unknown>;
104
+ }
package/src/index.ts ADDED
@@ -0,0 +1,517 @@
1
+ import type { BridgeClientCommand, BridgeCommandHelpers, BridgeCommandOptions } from "./commands";
2
+ import type { BridgeFrame } from "./reference-consumer";
3
+
4
+ export * from "./commands";
5
+ export * from "./reference-consumer";
6
+ export * from "./workflow-gate";
7
+
8
+ import type { UnattendedDeclaration, WorkflowGate, WorkflowGateResolver } from "./workflow-gate";
9
+ import { isWorkflowGateFrame } from "./workflow-gate";
10
+ export type BridgeCapability =
11
+ | "events"
12
+ | "prompt"
13
+ | "permission"
14
+ | "elicitation"
15
+ | "ui.declarative"
16
+ | "ui.editor"
17
+ | "ui.terminal_input"
18
+ | "host_tools"
19
+ | "host_uri"
20
+ | "client_bridge.read_text_file"
21
+ | "client_bridge.write_text_file"
22
+ | "client_bridge.create_terminal"
23
+ | "workflow_gate";
24
+
25
+ export type BridgeCommandScope =
26
+ | "prompt"
27
+ | "control"
28
+ | "bash"
29
+ | "export"
30
+ | "session"
31
+ | "model"
32
+ | "message:read"
33
+ | "host_tools"
34
+ | "host_uri"
35
+ | "admin";
36
+
37
+ export interface BridgeProtocolRange {
38
+ min: number;
39
+ max: number;
40
+ }
41
+
42
+ export interface BridgeHandshakeRequest {
43
+ protocol_version_range: BridgeProtocolRange;
44
+ capabilities: BridgeCapability[];
45
+ requested_scopes: BridgeCommandScope[];
46
+ last_seq?: number;
47
+ unattended?: UnattendedDeclaration;
48
+ }
49
+
50
+ export interface BridgeHandshakeAccepted {
51
+ status: "accepted";
52
+ protocol_version: number;
53
+ session_id: string;
54
+ accepted_capabilities: BridgeCapability[];
55
+ accepted_scopes: BridgeCommandScope[];
56
+ unsupported: BridgeCapability[];
57
+ endpoints: {
58
+ events: string;
59
+ commands: string;
60
+ uiResponses: string;
61
+ claimControl: string;
62
+ hostToolResults: string;
63
+ disconnectControl: string;
64
+ hostUriResults: string;
65
+ };
66
+ frame_types: string[];
67
+ accepted_unattended?: UnattendedDeclaration;
68
+ }
69
+
70
+ export interface BridgeHandshakeRejected {
71
+ status: "rejected";
72
+ reason: "incompatible_version" | "unauthorized" | "invalid_request";
73
+ message: string;
74
+ }
75
+
76
+ export type BridgeFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
77
+ export type BridgeHandshakeResponse = BridgeHandshakeAccepted | BridgeHandshakeRejected;
78
+ function parseSseData(buffer: string): { frames: BridgeFrame[]; rest: string } {
79
+ const frames: BridgeFrame[] = [];
80
+ let rest = buffer.replaceAll("\r\n", "\n");
81
+ let boundary = rest.indexOf("\n\n");
82
+ while (boundary >= 0) {
83
+ const block = rest.slice(0, boundary);
84
+ rest = rest.slice(boundary + 2);
85
+ for (const line of block.split("\n")) {
86
+ if (!line.startsWith("data: ")) continue;
87
+ frames.push(JSON.parse(line.slice(6)) as BridgeFrame);
88
+ }
89
+ boundary = rest.indexOf("\n\n");
90
+ }
91
+ return { frames, rest };
92
+ }
93
+
94
+ export interface BridgeClientOptions {
95
+ baseUrl: string;
96
+ token: string;
97
+ fetch?: BridgeFetch;
98
+ allowInsecureLocalhost?: boolean;
99
+ }
100
+
101
+ function isLocalhostUrl(url: URL): boolean {
102
+ return url.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname);
103
+ }
104
+
105
+ export class BridgeClient implements BridgeCommandHelpers {
106
+ readonly #baseUrl: URL;
107
+ readonly #token: string;
108
+ readonly #fetch: BridgeFetch;
109
+
110
+ constructor(options: BridgeClientOptions) {
111
+ this.#baseUrl = new URL(options.baseUrl);
112
+ if (this.#baseUrl.protocol !== "https:" && !isLocalhostUrl(this.#baseUrl)) {
113
+ throw new Error("BridgeClient refuses bearer tokens over non-HTTPS bridge URLs");
114
+ }
115
+ if (isLocalhostUrl(this.#baseUrl) && !options.allowInsecureLocalhost) {
116
+ throw new Error(
117
+ "BridgeClient refuses bearer tokens over HTTP localhost unless allowInsecureLocalhost is true",
118
+ );
119
+ }
120
+ this.#token = options.token;
121
+ this.#fetch = options.fetch ?? fetch;
122
+ }
123
+
124
+ async handshake(request: BridgeHandshakeRequest): Promise<BridgeHandshakeResponse> {
125
+ return this.#json<BridgeHandshakeResponse>("/v1/handshake", {
126
+ method: "POST",
127
+ body: JSON.stringify(request),
128
+ headers: { "Content-Type": "application/json" },
129
+ });
130
+ }
131
+
132
+ async command(command: BridgeClientCommand, sessionId: string, idempotencyKey: string): Promise<unknown> {
133
+ return this.#json(`/v1/sessions/${encodeURIComponent(sessionId)}/commands`, {
134
+ method: "POST",
135
+ body: JSON.stringify(command),
136
+ headers: {
137
+ "Content-Type": "application/json",
138
+ "Idempotency-Key": idempotencyKey,
139
+ },
140
+ });
141
+ }
142
+
143
+ #command(
144
+ type: BridgeClientCommand["type"],
145
+ sessionId: string,
146
+ fields: Record<string, unknown> = {},
147
+ options: BridgeCommandOptions = {},
148
+ prefix: string = type,
149
+ ): Promise<unknown> {
150
+ return this.command(
151
+ { id: options.id, type, ...fields },
152
+ sessionId,
153
+ options.idempotencyKey ?? this.createIdempotencyKey(prefix),
154
+ );
155
+ }
156
+
157
+ prompt(
158
+ sessionId: string,
159
+ message: string,
160
+ options: {
161
+ id?: string;
162
+ images?: unknown[];
163
+ streamingBehavior?: "steer" | "followUp";
164
+ idempotencyKey?: string;
165
+ } = {},
166
+ ): Promise<unknown> {
167
+ return this.command(
168
+ {
169
+ id: options.id,
170
+ type: "prompt",
171
+ message,
172
+ images: options.images,
173
+ streamingBehavior: options.streamingBehavior,
174
+ },
175
+ sessionId,
176
+ options.idempotencyKey ?? this.createIdempotencyKey("prompt"),
177
+ );
178
+ }
179
+
180
+ steer(
181
+ sessionId: string,
182
+ message: string,
183
+ options: { id?: string; images?: unknown[]; idempotencyKey?: string } = {},
184
+ ): Promise<unknown> {
185
+ return this.command(
186
+ { id: options.id, type: "steer", message, images: options.images },
187
+ sessionId,
188
+ options.idempotencyKey ?? this.createIdempotencyKey("steer"),
189
+ );
190
+ }
191
+
192
+ followUp(
193
+ sessionId: string,
194
+ message: string,
195
+ options: { id?: string; images?: unknown[]; idempotencyKey?: string } = {},
196
+ ): Promise<unknown> {
197
+ return this.command(
198
+ { id: options.id, type: "follow_up", message, images: options.images },
199
+ sessionId,
200
+ options.idempotencyKey ?? this.createIdempotencyKey("follow-up"),
201
+ );
202
+ }
203
+
204
+ bash(sessionId: string, command: string, options: { id?: string; idempotencyKey?: string } = {}): Promise<unknown> {
205
+ return this.command(
206
+ { id: options.id, type: "bash", command },
207
+ sessionId,
208
+ options.idempotencyKey ?? this.createIdempotencyKey("bash"),
209
+ );
210
+ }
211
+
212
+ getState(sessionId: string, options: { id?: string; idempotencyKey?: string } = {}): Promise<unknown> {
213
+ return this.command(
214
+ { id: options.id, type: "get_state" },
215
+ sessionId,
216
+ options.idempotencyKey ?? this.createIdempotencyKey("get-state"),
217
+ );
218
+ }
219
+
220
+ getMessages(sessionId: string, options: { id?: string; idempotencyKey?: string } = {}): Promise<unknown> {
221
+ return this.command(
222
+ { id: options.id, type: "get_messages" },
223
+ sessionId,
224
+ options.idempotencyKey ?? this.createIdempotencyKey("get-messages"),
225
+ );
226
+ }
227
+
228
+ abort(sessionId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
229
+ return this.#command("abort", sessionId, {}, options);
230
+ }
231
+
232
+ abortAndPrompt(
233
+ sessionId: string,
234
+ message: string,
235
+ options: { id?: string; images?: unknown[]; idempotencyKey?: string } = {},
236
+ ): Promise<unknown> {
237
+ return this.#command(
238
+ "abort_and_prompt",
239
+ sessionId,
240
+ { message, images: options.images },
241
+ options,
242
+ "abort-and-prompt",
243
+ );
244
+ }
245
+
246
+ newSession(sessionId: string, options: BridgeCommandOptions & { parentSession?: string } = {}): Promise<unknown> {
247
+ return this.#command("new_session", sessionId, { parentSession: options.parentSession }, options, "new-session");
248
+ }
249
+
250
+ setTodos(sessionId: string, phases: unknown[], options: BridgeCommandOptions = {}): Promise<unknown> {
251
+ return this.#command("set_todos", sessionId, { phases }, options, "set-todos");
252
+ }
253
+
254
+ setHostTools(sessionId: string, tools: unknown[], options: BridgeCommandOptions = {}): Promise<unknown> {
255
+ return this.#command("set_host_tools", sessionId, { tools }, options, "set-host-tools");
256
+ }
257
+
258
+ setHostUriSchemes(sessionId: string, schemes: unknown[], options: BridgeCommandOptions = {}): Promise<unknown> {
259
+ return this.#command("set_host_uri_schemes", sessionId, { schemes }, options, "set-host-uri-schemes");
260
+ }
261
+
262
+ setModel(
263
+ sessionId: string,
264
+ provider: string,
265
+ modelId: string,
266
+ options: BridgeCommandOptions = {},
267
+ ): Promise<unknown> {
268
+ return this.#command("set_model", sessionId, { provider, modelId }, options, "set-model");
269
+ }
270
+
271
+ cycleModel(sessionId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
272
+ return this.#command("cycle_model", sessionId, {}, options, "cycle-model");
273
+ }
274
+
275
+ getAvailableModels(sessionId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
276
+ return this.#command("get_available_models", sessionId, {}, options, "get-available-models");
277
+ }
278
+
279
+ setThinkingLevel(sessionId: string, level: string, options: BridgeCommandOptions = {}): Promise<unknown> {
280
+ return this.#command("set_thinking_level", sessionId, { level }, options, "set-thinking-level");
281
+ }
282
+
283
+ cycleThinkingLevel(sessionId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
284
+ return this.#command("cycle_thinking_level", sessionId, {}, options, "cycle-thinking-level");
285
+ }
286
+
287
+ setSteeringMode(
288
+ sessionId: string,
289
+ mode: "all" | "one-at-a-time",
290
+ options: BridgeCommandOptions = {},
291
+ ): Promise<unknown> {
292
+ return this.#command("set_steering_mode", sessionId, { mode }, options, "set-steering-mode");
293
+ }
294
+
295
+ setFollowUpMode(
296
+ sessionId: string,
297
+ mode: "all" | "one-at-a-time",
298
+ options: BridgeCommandOptions = {},
299
+ ): Promise<unknown> {
300
+ return this.#command("set_follow_up_mode", sessionId, { mode }, options, "set-follow-up-mode");
301
+ }
302
+
303
+ setInterruptMode(
304
+ sessionId: string,
305
+ mode: "immediate" | "wait",
306
+ options: BridgeCommandOptions = {},
307
+ ): Promise<unknown> {
308
+ return this.#command("set_interrupt_mode", sessionId, { mode }, options, "set-interrupt-mode");
309
+ }
310
+
311
+ compact(sessionId: string, options: BridgeCommandOptions & { customInstructions?: string } = {}): Promise<unknown> {
312
+ return this.#command("compact", sessionId, { customInstructions: options.customInstructions }, options);
313
+ }
314
+
315
+ setAutoCompaction(sessionId: string, enabled: boolean, options: BridgeCommandOptions = {}): Promise<unknown> {
316
+ return this.#command("set_auto_compaction", sessionId, { enabled }, options, "set-auto-compaction");
317
+ }
318
+
319
+ setAutoRetry(sessionId: string, enabled: boolean, options: BridgeCommandOptions = {}): Promise<unknown> {
320
+ return this.#command("set_auto_retry", sessionId, { enabled }, options, "set-auto-retry");
321
+ }
322
+
323
+ abortRetry(sessionId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
324
+ return this.#command("abort_retry", sessionId, {}, options, "abort-retry");
325
+ }
326
+
327
+ abortBash(sessionId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
328
+ return this.#command("abort_bash", sessionId, {}, options, "abort-bash");
329
+ }
330
+
331
+ getSessionStats(sessionId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
332
+ return this.#command("get_session_stats", sessionId, {}, options, "get-session-stats");
333
+ }
334
+
335
+ exportHtml(sessionId: string, options: BridgeCommandOptions & { outputPath?: string } = {}): Promise<unknown> {
336
+ return this.#command("export_html", sessionId, { outputPath: options.outputPath }, options, "export-html");
337
+ }
338
+
339
+ switchSession(sessionId: string, sessionPath: string, options: BridgeCommandOptions = {}): Promise<unknown> {
340
+ return this.#command("switch_session", sessionId, { sessionPath }, options, "switch-session");
341
+ }
342
+
343
+ branch(sessionId: string, entryId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
344
+ return this.#command("branch", sessionId, { entryId }, options);
345
+ }
346
+
347
+ getBranchMessages(sessionId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
348
+ return this.#command("get_branch_messages", sessionId, {}, options, "get-branch-messages");
349
+ }
350
+
351
+ getLastAssistantText(sessionId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
352
+ return this.#command("get_last_assistant_text", sessionId, {}, options, "get-last-assistant-text");
353
+ }
354
+
355
+ setSessionName(sessionId: string, name: string, options: BridgeCommandOptions = {}): Promise<unknown> {
356
+ return this.#command("set_session_name", sessionId, { name }, options, "set-session-name");
357
+ }
358
+
359
+ handoff(sessionId: string, options: BridgeCommandOptions & { customInstructions?: string } = {}): Promise<unknown> {
360
+ return this.#command("handoff", sessionId, { customInstructions: options.customInstructions }, options);
361
+ }
362
+
363
+ getLoginProviders(sessionId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
364
+ return this.#command("get_login_providers", sessionId, {}, options, "get-login-providers");
365
+ }
366
+
367
+ login(sessionId: string, providerId: string, options: BridgeCommandOptions = {}): Promise<unknown> {
368
+ return this.#command("login", sessionId, { providerId }, options);
369
+ }
370
+
371
+ createIdempotencyKey(prefix = "cmd"): string {
372
+ return `${prefix}-${crypto.randomUUID()}`;
373
+ }
374
+
375
+ async *events(sessionId: string, lastSeq?: number): AsyncGenerator<BridgeFrame> {
376
+ const response = await this.connectEvents(sessionId, lastSeq);
377
+ if (!response.ok) throw new Error(`Bridge event stream failed: ${response.status}`);
378
+ const reader = response.body?.getReader();
379
+ if (!reader) throw new Error("Bridge event stream response had no body");
380
+ const decoder = new TextDecoder();
381
+ let buffered = "";
382
+ try {
383
+ while (true) {
384
+ const chunk = await reader.read();
385
+ if (chunk.done) break;
386
+ buffered += decoder.decode(chunk.value, { stream: true });
387
+ const parsed = parseSseData(buffered);
388
+ buffered = parsed.rest;
389
+ for (const frame of parsed.frames) yield frame;
390
+ }
391
+ buffered += decoder.decode();
392
+ const parsed = parseSseData(buffered);
393
+ for (const frame of parsed.frames) yield frame;
394
+ } finally {
395
+ await reader.cancel().catch(() => undefined);
396
+ reader.releaseLock();
397
+ }
398
+ }
399
+ claimControl(sessionId: string, ownerToken?: string): Promise<unknown> {
400
+ return this.#json(`/v1/sessions/${encodeURIComponent(sessionId)}/control:claim`, {
401
+ method: "POST",
402
+ headers: ownerToken ? { "X-GJC-Bridge-Owner-Token": ownerToken } : undefined,
403
+ });
404
+ }
405
+ disconnectControl(sessionId: string, ownerToken: string): Promise<unknown> {
406
+ return this.#json(`/v1/sessions/${encodeURIComponent(sessionId)}/control:disconnect`, {
407
+ method: "POST",
408
+ headers: { "X-GJC-Bridge-Owner-Token": ownerToken },
409
+ });
410
+ }
411
+
412
+ respondToUiRequest(
413
+ sessionId: string,
414
+ correlationId: string,
415
+ ownerToken: string,
416
+ response: unknown,
417
+ idempotencyKey?: string,
418
+ ): Promise<unknown> {
419
+ return this.#json(
420
+ `/v1/sessions/${encodeURIComponent(sessionId)}/ui-responses/${encodeURIComponent(correlationId)}`,
421
+ {
422
+ method: "POST",
423
+ body: JSON.stringify(response),
424
+ headers: {
425
+ "Content-Type": "application/json",
426
+ "X-GJC-Bridge-Owner-Token": ownerToken,
427
+ ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
428
+ },
429
+ },
430
+ );
431
+ }
432
+
433
+ /**
434
+ * Answer a `workflow_gate` by posting to the UI-response endpoint and return
435
+ * the gate resolution envelope. Authorization is bearer auth plus the
436
+ * `control` scope; `ownerToken` is carried for idempotency/controller
437
+ * correlation, not as the gate authorization boundary.
438
+ */
439
+ respondGate(
440
+ sessionId: string,
441
+ gateId: string,
442
+ ownerToken: string,
443
+ answer: unknown,
444
+ options: { idempotencyKey?: string; id?: string } = {},
445
+ ): Promise<unknown> {
446
+ return this.#json(`/v1/sessions/${encodeURIComponent(sessionId)}/ui-responses/${encodeURIComponent(gateId)}`, {
447
+ method: "POST",
448
+ body: JSON.stringify({ gate_id: gateId, answer, idempotency_key: options.idempotencyKey }),
449
+ headers: {
450
+ "Content-Type": "application/json",
451
+ "X-GJC-Bridge-Owner-Token": ownerToken,
452
+ ...(options.idempotencyKey ? { "Idempotency-Key": options.idempotencyKey } : {}),
453
+ },
454
+ });
455
+ }
456
+
457
+ /**
458
+ * Headless policy: stream the session's frames, route every received
459
+ * `workflow_gate` to the agent `resolver`, and post its answer back. Yields
460
+ * each handled gate. The resolver supplies the agent's memory-backed answer.
461
+ */
462
+ async *consumeWorkflowGates(
463
+ sessionId: string,
464
+ ownerToken: string,
465
+ resolver: WorkflowGateResolver,
466
+ options: { lastSeq?: number } = {},
467
+ ): AsyncGenerator<{ gate: WorkflowGate; answer: unknown }> {
468
+ for await (const frame of this.events(sessionId, options.lastSeq)) {
469
+ if (!isWorkflowGateFrame(frame)) continue;
470
+ const gate = frame.payload as WorkflowGate;
471
+ const answer = await resolver(gate);
472
+ await this.respondGate(sessionId, gate.gate_id, ownerToken, answer);
473
+ yield { gate, answer };
474
+ }
475
+ }
476
+
477
+ respondToHostTool(sessionId: string, correlationId: string, result: unknown): Promise<unknown> {
478
+ return this.#json(
479
+ `/v1/sessions/${encodeURIComponent(sessionId)}/host-tool-results/${encodeURIComponent(correlationId)}`,
480
+ {
481
+ method: "POST",
482
+ body: JSON.stringify(result),
483
+ headers: { "Content-Type": "application/json" },
484
+ },
485
+ );
486
+ }
487
+
488
+ respondToHostUri(sessionId: string, correlationId: string, result: unknown): Promise<unknown> {
489
+ return this.#json(
490
+ `/v1/sessions/${encodeURIComponent(sessionId)}/host-uri-results/${encodeURIComponent(correlationId)}`,
491
+ {
492
+ method: "POST",
493
+ body: JSON.stringify(result),
494
+ headers: { "Content-Type": "application/json" },
495
+ },
496
+ );
497
+ }
498
+ connectEvents(sessionId: string, lastSeq?: number): Promise<Response> {
499
+ const path = `/v1/sessions/${encodeURIComponent(sessionId)}/events${lastSeq === undefined ? "" : `?last_seq=${lastSeq}`}`;
500
+ return this.#request(path, { method: "GET" });
501
+ }
502
+
503
+ #request(pathname: string, init: RequestInit): Promise<Response> {
504
+ const url = new URL(pathname, this.#baseUrl);
505
+ const headers = new Headers(init.headers);
506
+ headers.set("Authorization", `Bearer ${this.#token}`);
507
+ return this.#fetch(url, { ...init, headers });
508
+ }
509
+
510
+ async #json<T>(pathname: string, init: RequestInit): Promise<T> {
511
+ const response = await this.#request(pathname, init);
512
+ if (!response.ok) {
513
+ throw new Error(`Bridge request failed: ${response.status}`);
514
+ }
515
+ return (await response.json()) as T;
516
+ }
517
+ }
@@ -0,0 +1,56 @@
1
+ export interface BridgeFrame<TPayload = unknown> {
2
+ protocol_version: number;
3
+ session_id: string;
4
+ seq: number;
5
+ frame_id: string;
6
+ correlation_id?: string;
7
+ type: string;
8
+ payload: TPayload;
9
+ }
10
+
11
+ export interface RenderedBridgeFrame {
12
+ seq: number;
13
+ type: string;
14
+ html: string;
15
+ }
16
+
17
+ function escapeHtml(value: string): string {
18
+ return value
19
+ .replaceAll("&", "&amp;")
20
+ .replaceAll("<", "&lt;")
21
+ .replaceAll(">", "&gt;")
22
+ .replaceAll('"', "&quot;")
23
+ .replaceAll("'", "&#39;");
24
+ }
25
+
26
+ function payloadSummary(payload: unknown): string {
27
+ if (!payload || typeof payload !== "object") return String(payload ?? "");
28
+ if ("event_type" in payload && typeof payload.event_type === "string") return payload.event_type;
29
+ if ("kind" in payload && typeof payload.kind === "string") return payload.kind;
30
+ if ("command" in payload && typeof payload.command === "string") return payload.command;
31
+ return JSON.stringify(payload);
32
+ }
33
+
34
+ export function renderBridgeFrame(frame: BridgeFrame): RenderedBridgeFrame {
35
+ const summary = escapeHtml(payloadSummary(frame.payload));
36
+ const correlation = frame.correlation_id ? ` data-correlation="${escapeHtml(frame.correlation_id)}"` : "";
37
+ return {
38
+ seq: frame.seq,
39
+ type: frame.type,
40
+ html: `<article class="bridge-frame bridge-frame-${escapeHtml(frame.type)}" data-seq="${frame.seq}"${correlation}><h3>${escapeHtml(frame.type)}</h3><pre>${summary}</pre></article>`,
41
+ };
42
+ }
43
+
44
+ export class ReferenceBridgeConsumer {
45
+ #frames: RenderedBridgeFrame[] = [];
46
+
47
+ consume(frame: BridgeFrame): RenderedBridgeFrame {
48
+ const rendered = renderBridgeFrame(frame);
49
+ this.#frames.push(rendered);
50
+ return rendered;
51
+ }
52
+
53
+ renderDocument(): string {
54
+ return `<!doctype html><html><body>${this.#frames.map(frame => frame.html).join("")}</body></html>`;
55
+ }
56
+ }
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Typed `workflow_gate` client helpers (#322).
3
+ *
4
+ * Mirrors the server-side workflow-gate contract for bridge consumers: a typed
5
+ * gate frame, the response shape, a frame type-guard, and a headless policy that
6
+ * routes received gates to an agent callback and posts answers back through the
7
+ * existing owner-token ui-response flow.
8
+ */
9
+ import type { BridgeFrame } from "./reference-consumer";
10
+
11
+ export type WorkflowGateStage = "deep-interview" | "ralplan" | "ultragoal";
12
+ export type WorkflowGateKind = "question" | "approval" | "execution";
13
+
14
+ export interface WorkflowGateOption {
15
+ value: unknown;
16
+ label: string;
17
+ description?: string;
18
+ }
19
+
20
+ export interface WorkflowGate {
21
+ type: "workflow_gate";
22
+ gate_id: string;
23
+ stage: WorkflowGateStage;
24
+ kind: WorkflowGateKind;
25
+ schema: unknown;
26
+ schema_hash: string;
27
+ options?: WorkflowGateOption[];
28
+ context: Record<string, unknown>;
29
+ created_at: string;
30
+ required: true;
31
+ }
32
+
33
+ export interface WorkflowGateResponse {
34
+ gate_id: string;
35
+ answer: unknown;
36
+ idempotency_key?: string;
37
+ }
38
+
39
+ /** Unattended declaration carried on the bridge handshake (#318/#319). */
40
+ export interface UnattendedDeclaration {
41
+ actor: string;
42
+ budget: {
43
+ max_tokens: number;
44
+ max_tool_calls: number;
45
+ max_wall_time_ms: number;
46
+ max_cost_usd: number;
47
+ };
48
+ scopes: string[];
49
+ action_allowlist: string[];
50
+ }
51
+
52
+ /** Type guard: is this bridge frame a fully-formed workflow_gate frame? */
53
+ export function isWorkflowGateFrame(frame: BridgeFrame): frame is BridgeFrame<WorkflowGate> {
54
+ if (frame.type !== "workflow_gate") return false;
55
+ const p = frame.payload as Partial<WorkflowGate> | undefined;
56
+ if (!p || typeof p !== "object") return false;
57
+ const stages: WorkflowGateStage[] = ["deep-interview", "ralplan", "ultragoal"];
58
+ const kinds: WorkflowGateKind[] = ["question", "approval", "execution"];
59
+ return (
60
+ p.type === "workflow_gate" &&
61
+ typeof p.gate_id === "string" &&
62
+ typeof p.stage === "string" &&
63
+ stages.includes(p.stage as WorkflowGateStage) &&
64
+ typeof p.kind === "string" &&
65
+ kinds.includes(p.kind as WorkflowGateKind) &&
66
+ typeof p.schema_hash === "string" &&
67
+ typeof p.created_at === "string" &&
68
+ p.required === true &&
69
+ "schema" in p &&
70
+ typeof p.context === "object" &&
71
+ p.context !== null &&
72
+ (p.options === undefined || Array.isArray(p.options))
73
+ );
74
+ }
75
+
76
+ /** A callback that produces an answer for a received gate (the agent's "memory"). */
77
+ export type WorkflowGateResolver = (gate: WorkflowGate) => unknown | Promise<unknown>;