work-ally 0.2.0-alpha.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/AGENTS.md +110 -0
- package/DASHBOARD.md +160 -0
- package/PRODUCT.md +113 -0
- package/README.md +403 -0
- package/ally.sh +171 -0
- package/bridge/src/approval-rules.ts +360 -0
- package/bridge/src/channel-delivery.ts +207 -0
- package/bridge/src/channel-types.ts +22 -0
- package/bridge/src/channels/fake/adapter.ts +31 -0
- package/bridge/src/channels/feishu/adapter.ts +411 -0
- package/bridge/src/channels/feishu/approvals.ts +6 -0
- package/bridge/src/channels/feishu/formatter.ts +276 -0
- package/bridge/src/channels/feishu/normalize.ts +368 -0
- package/bridge/src/codex-config.ts +52 -0
- package/bridge/src/config.ts +240 -0
- package/bridge/src/fake-runtime-client.ts +505 -0
- package/bridge/src/handoff-service.ts +494 -0
- package/bridge/src/logger.ts +194 -0
- package/bridge/src/memory-digest.ts +186 -0
- package/bridge/src/receiver-approval-autonomy.ts +158 -0
- package/bridge/src/receiver-control-core.ts +140 -0
- package/bridge/src/receiver-control-work-session.ts +218 -0
- package/bridge/src/receiver-control.ts +83 -0
- package/bridge/src/receiver-delivery.ts +136 -0
- package/bridge/src/receiver-helpers.ts +96 -0
- package/bridge/src/receiver-human-gate.ts +333 -0
- package/bridge/src/receiver-inbound-preflight.ts +162 -0
- package/bridge/src/receiver-recovery.ts +236 -0
- package/bridge/src/receiver-runtime-callbacks.ts +367 -0
- package/bridge/src/receiver-runtime-policy.ts +132 -0
- package/bridge/src/receiver-runtime-state.ts +124 -0
- package/bridge/src/receiver-support-actions.ts +189 -0
- package/bridge/src/receiver-thread-start.ts +57 -0
- package/bridge/src/receiver-turn-coordination.ts +94 -0
- package/bridge/src/receiver-turn-execution.ts +257 -0
- package/bridge/src/receiver-turn-failure.ts +143 -0
- package/bridge/src/receiver-turn-result.ts +185 -0
- package/bridge/src/receiver-turn-steer.ts +70 -0
- package/bridge/src/receiver-work-session.ts +76 -0
- package/bridge/src/receiver.ts +329 -0
- package/bridge/src/router.ts +62 -0
- package/bridge/src/runtime-client-agent-messages.ts +150 -0
- package/bridge/src/runtime-client-message-dispatch.ts +176 -0
- package/bridge/src/runtime-client-protocol.ts +411 -0
- package/bridge/src/runtime-client-request-ops.ts +56 -0
- package/bridge/src/runtime-client-run-turn.ts +158 -0
- package/bridge/src/runtime-client-thread-ops.ts +270 -0
- package/bridge/src/runtime-client-transport.ts +309 -0
- package/bridge/src/runtime-client-turn-poll.ts +224 -0
- package/bridge/src/runtime-client-turn-read.ts +185 -0
- package/bridge/src/runtime-client-turn-state.ts +105 -0
- package/bridge/src/runtime-client.ts +344 -0
- package/bridge/src/runtime-user-input.ts +403 -0
- package/bridge/src/scheduler.ts +239 -0
- package/bridge/src/server-handoff-command.ts +364 -0
- package/bridge/src/server-main.ts +80 -0
- package/bridge/src/server-routine-command.ts +60 -0
- package/bridge/src/server-routine-execution.ts +222 -0
- package/bridge/src/server-runtime-app-support.ts +107 -0
- package/bridge/src/server-runtime-app.ts +238 -0
- package/bridge/src/server-thread-sync-command.ts +63 -0
- package/bridge/src/server.ts +17 -0
- package/bridge/src/session-store-delivery.ts +220 -0
- package/bridge/src/session-store-human-gate.ts +380 -0
- package/bridge/src/session-store-inbound-acceptance.ts +66 -0
- package/bridge/src/session-store-meta.ts +134 -0
- package/bridge/src/session-store-turn-ledger.ts +272 -0
- package/bridge/src/session-store.ts +380 -0
- package/bridge/src/system-notify.ts +220 -0
- package/bridge/src/thread-sync.ts +200 -0
- package/bridge/src/translator.ts +494 -0
- package/bridge/src/types.ts +289 -0
- package/bridge/src/utils.ts +104 -0
- package/bridge/src/work-session-store.ts +471 -0
- package/docs/.gitkeep +0 -0
- package/docs/architecture/codex-feishu-bridge-proposal.md +2742 -0
- package/docs/completed/FEATURE-feishu-markdown-and-reply-support.md +327 -0
- package/docs/completed/README.md +21 -0
- package/docs/completed/SPEC-approval-autonomy-and-safe-defaults.md +205 -0
- package/docs/completed/SPEC-approval-batch-and-strict-reply-shortcuts.md +153 -0
- package/docs/completed/SPEC-conversation-noise-reduction-and-busy-input-gate.md +538 -0
- package/docs/completed/SPEC-engineering-sop-skillization.md +190 -0
- package/docs/completed/SPEC-faithful-bridge-core-thinning-v2.md +376 -0
- package/docs/completed/SPEC-faithful-bridge-core-thinning.md +1071 -0
- package/docs/completed/SPEC-group-chat-sender-identity.md +301 -0
- package/docs/completed/SPEC-middleware-exception-visibility.md +227 -0
- package/docs/completed/SPEC-nightly-memory-digest-visibility.md +121 -0
- package/docs/completed/SPEC-project-group-chat-human-centered-conversation-mapping.md +326 -0
- package/docs/completed/SPEC-remove-cli-persona-bootstrap.md +201 -0
- package/docs/developer-workflow.md +49 -0
- package/docs/implementation/SPEC-codex-same-machine-session-handoff-implementation.md +239 -0
- package/docs/implementation/test-coverage-map.md +363 -0
- package/docs/implementation/work-ally-implementation-guide.md +790 -0
- package/docs/issues/README.md +10 -0
- package/docs/issues/pending/ANALYSIS-ally-premature-recovery-notice-and-task-state-semantics-2026-03-18.md +295 -0
- package/docs/issues/resolved/ANALYSIS-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +466 -0
- package/docs/issues/resolved/ANALYSIS-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +261 -0
- package/docs/issues/resolved/ANALYSIS-codex-app-server-transport-disconnect-semantics-2026-03-14.md +606 -0
- package/docs/issues/resolved/ANALYSIS-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +348 -0
- package/docs/issues/resolved/ANALYSIS-runtime-turn-delivery-and-recovery-2026-03-14.md +603 -0
- package/docs/issues/resolved/ANALYSIS-self-test-gap-approval-waiting-visible-but-approval-artifact-missing-2026-03-16.md +166 -0
- package/docs/issues/resolved/ANALYSIS-self-test-gap-blocking-state-visible-without-user-actionable-artifact-2026-03-16.md +186 -0
- package/docs/issues/resolved/ANALYSIS-self-test-gap-premature-terminalization-on-fresh-thread-poll-and-object-error-leak-2026-03-16.md +166 -0
- package/docs/issues/resolved/REPORT-ally-runtime-turn-delivery-3b42fb8-2026-03-15.md +373 -0
- package/docs/manual-acceptance.md +127 -0
- package/docs/ops-runbook.md +44 -0
- package/docs/planning/FEATURE-memory-system.md +748 -0
- package/docs/planning/SPEC-active-turn-steer-and-context-compaction-visibility.md +269 -0
- package/docs/planning/SPEC-approval-rules-inheritance-and-local-validation-lane.md +450 -0
- package/docs/planning/SPEC-assistant-persona-bootstrap.md +199 -0
- package/docs/planning/SPEC-assistant-rename.md +610 -0
- package/docs/planning/SPEC-bridge-app-server-protocol-alignment.md +667 -0
- package/docs/planning/SPEC-claude-runtime-host-for-work-ally.md +434 -0
- package/docs/planning/SPEC-cli-feishu-codex-session-unification.md +236 -0
- package/docs/planning/SPEC-codex-same-machine-session-handoff.md +873 -0
- package/docs/planning/SPEC-feishu-reaction-shortcuts.md +282 -0
- package/docs/planning/SPEC-local-stable-release-boundary.md +166 -0
- package/docs/planning/SPEC-managed-thread-entry-and-surface-mobility.md +862 -0
- package/docs/planning/SPEC-minimal-bridge-semantics-and-user-visible-surface.md +362 -0
- package/docs/planning/SPEC-npm-alpha-distribution-and-install-first-release.md +222 -0
- package/docs/planning/SPEC-remove-websocket-runtime-transport.md +364 -0
- package/docs/planning/SPEC-runtime-abstraction-phase-1.md +424 -0
- package/docs/planning/SPEC-runtime-connection-and-turn-recovery-semantics.md +274 -0
- package/docs/planning/SPEC-session-presence-and-state-visibility.md +397 -0
- package/docs/planning/SPEC-skill-first-capability-packaging.md +338 -0
- package/docs/planning/SPEC-stable-archive-contract.md +456 -0
- package/docs/planning/SPEC-supervised-start-boundary.md +127 -0
- package/docs/planning/SPEC-user-barrier-reduction-and-activation.md +832 -0
- package/docs/planning/ally-next.md +1278 -0
- package/docs/planning/assistant-workbench-spec.md +725 -0
- package/docs/planning/product-workbench.md +283 -0
- package/docs/product-onboarding.md +227 -0
- package/docs/product-spec-standard.md +528 -0
- package/docs/troubleshooting.md +45 -0
- package/docs/user-quickstart.md +46 -0
- package/internal/dispatch.sh +95 -0
- package/internal/lib/common.sh +1450 -0
- package/internal/modules/assistant/manage.sh +1312 -0
- package/internal/modules/bootstrap/setup.sh +144 -0
- package/internal/modules/config/init-env.sh +10 -0
- package/internal/modules/global/manage.sh +154 -0
- package/internal/modules/handoff/manage.sh +54 -0
- package/internal/modules/mcp/manage.sh +83 -0
- package/internal/modules/ops/logs.sh +76 -0
- package/internal/modules/routines/manage.sh +55 -0
- package/internal/modules/runtime/assistant-autosave.sh +26 -0
- package/internal/modules/runtime/restart.sh +6 -0
- package/internal/modules/runtime/start.sh +283 -0
- package/internal/modules/runtime/status.sh +194 -0
- package/internal/modules/runtime/stop.sh +55 -0
- package/internal/modules/runtime/supervisor.sh +216 -0
- package/internal/modules/runtime/update.sh +26 -0
- package/package.json +41 -0
- package/runtime/config/.gitkeep +0 -0
- package/runtime/host/.gitkeep +0 -0
- package/runtime/host/healthcheck-codex-app-server.ts +22 -0
- package/runtime/host/ping-pong-codex-app-server.ts +66 -0
- package/runtime/host/probe-codex-app-server.ts +115 -0
- package/skills/archive-reader/SKILL.md +9 -0
- package/skills/feishu-production-debug/SKILL.md +37 -0
- package/skills/feishu-production-debug/references/feishu-debug-order.md +49 -0
- package/skills/feishu-production-debug/references/platform-permission-baseline.md +23 -0
- package/skills/issue-to-spec-triage/SKILL.md +44 -0
- package/skills/issue-to-spec-triage/references/triage-rules.md +66 -0
- package/skills/memory-digest/SKILL.md +9 -0
- package/skills/post-implementation-closure/SKILL.md +39 -0
- package/skills/post-implementation-closure/references/closure-checklist.md +45 -0
- package/skills/post-implementation-closure/references/doc-drift-map.md +49 -0
- package/skills/product-spec/SKILL.md +244 -0
- package/templates/env.example +5 -0
- package/templates/routines/nightly-memory-digest.yaml +10 -0
- package/templates/workspace/AGENTS.md +26 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { buildReplyFromThreadItems, observeContextCompactionEvent, type RuntimeTurnState } from './runtime-client-agent-messages.ts';
|
|
2
|
+
import { buildTurnResultFromObject, mergeTerminalTurnResult } from './runtime-client-turn-read.ts';
|
|
3
|
+
import { logDebug, logInfo, logWarn } from './logger.ts';
|
|
4
|
+
import {
|
|
5
|
+
approvalRequestFrom,
|
|
6
|
+
normalizeRuntimeError,
|
|
7
|
+
normalizeThreadStatus,
|
|
8
|
+
serverRequestResolvedEventFrom,
|
|
9
|
+
userInputRequestFrom,
|
|
10
|
+
type JsonRpcMessage,
|
|
11
|
+
} from './runtime-client-protocol.ts';
|
|
12
|
+
import type { RuntimeThreadStatus, RuntimeTurnResult, ServerRequestResolvedEvent } from './types.ts';
|
|
13
|
+
|
|
14
|
+
export interface PendingRuntimeRequest {
|
|
15
|
+
resolve: (value: any) => void;
|
|
16
|
+
reject: (error: any) => void;
|
|
17
|
+
timer: NodeJS.Timeout;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RuntimeMessageDispatchDeps {
|
|
21
|
+
pending: Map<string | number, PendingRuntimeRequest>;
|
|
22
|
+
turnStates: Map<string, RuntimeTurnState>;
|
|
23
|
+
cacheThreadStatus: (threadId: string, status: RuntimeThreadStatus | null | undefined) => RuntimeThreadStatus | null;
|
|
24
|
+
emitServerRequestResolved: (event: ServerRequestResolvedEvent) => void;
|
|
25
|
+
emitServerRequest: (msg: JsonRpcMessage) => void;
|
|
26
|
+
emitContextCompactionProgress: (turnId: string, itemId: string) => void;
|
|
27
|
+
resolveTurnResult: (turnId: string, result: RuntimeTurnResult) => boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function handleRuntimeRpcMessage(msg: JsonRpcMessage, deps: RuntimeMessageDispatchDeps): void {
|
|
31
|
+
if (msg.id != null && !msg.method) {
|
|
32
|
+
handlePendingRequestResolution(msg, deps.pending);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const resolvedRequest = serverRequestResolvedEventFrom(msg);
|
|
37
|
+
if (resolvedRequest) {
|
|
38
|
+
handleServerRequestResolved(resolvedRequest, deps);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (msg.id != null && msg.method) {
|
|
43
|
+
handleServerRequest(msg, deps);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!msg.method) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (observeContextCompactionEvent(deps.turnStates, msg, deps.emitContextCompactionProgress)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (msg.method === 'thread/status/changed') {
|
|
56
|
+
handleThreadStatusChanged(msg, deps);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (msg.method === 'turn/completed') {
|
|
61
|
+
handleTurnCompleted(msg, deps);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function handlePendingRequestResolution(
|
|
66
|
+
msg: JsonRpcMessage,
|
|
67
|
+
pendingRequests: Map<string | number, PendingRuntimeRequest>,
|
|
68
|
+
): void {
|
|
69
|
+
const pending = pendingRequests.get(msg.id!);
|
|
70
|
+
if (!pending) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
pendingRequests.delete(msg.id!);
|
|
74
|
+
clearTimeout(pending.timer);
|
|
75
|
+
if (msg.error) {
|
|
76
|
+
const runtimeError = normalizeRuntimeError(msg.error);
|
|
77
|
+
logWarn('runtime', 'request_failed', { id: msg.id, error: runtimeError.stack || runtimeError.message, rawError: msg.error });
|
|
78
|
+
pending.reject(runtimeError);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
logDebug('runtime', 'request_resolved', { id: msg.id });
|
|
82
|
+
pending.resolve(msg.result);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function handleServerRequestResolved(
|
|
86
|
+
event: ServerRequestResolvedEvent,
|
|
87
|
+
deps: RuntimeMessageDispatchDeps,
|
|
88
|
+
): void {
|
|
89
|
+
if (event.threadId) {
|
|
90
|
+
deps.cacheThreadStatus(event.threadId, {
|
|
91
|
+
type: 'active',
|
|
92
|
+
activeFlags: [],
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
logInfo('runtime', 'server_request_resolved', {
|
|
96
|
+
requestId: event.requestId,
|
|
97
|
+
threadId: event.threadId,
|
|
98
|
+
turnId: event.turnId,
|
|
99
|
+
});
|
|
100
|
+
deps.emitServerRequestResolved(event);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleServerRequest(msg: JsonRpcMessage, deps: RuntimeMessageDispatchDeps): void {
|
|
104
|
+
const approval = approvalRequestFrom(msg, '');
|
|
105
|
+
const userInput = userInputRequestFrom(msg, '');
|
|
106
|
+
const threadId = approval?.threadId || userInput?.threadId || (typeof msg.params?.threadId === 'string' ? msg.params.threadId : null);
|
|
107
|
+
const turnId = approval?.turnId || userInput?.turnId || (typeof msg.params?.turnId === 'string' ? msg.params.turnId : null);
|
|
108
|
+
|
|
109
|
+
if (approval) {
|
|
110
|
+
deps.cacheThreadStatus(approval.threadId, {
|
|
111
|
+
type: 'active',
|
|
112
|
+
activeFlags: ['waitingOnApproval'],
|
|
113
|
+
});
|
|
114
|
+
} else if (userInput) {
|
|
115
|
+
deps.cacheThreadStatus(userInput.threadId, {
|
|
116
|
+
type: 'active',
|
|
117
|
+
activeFlags: ['waitingOnUserInput'],
|
|
118
|
+
});
|
|
119
|
+
} else if (msg.method === 'mcpServer/elicitation/request' && threadId) {
|
|
120
|
+
deps.cacheThreadStatus(threadId, {
|
|
121
|
+
type: 'active',
|
|
122
|
+
activeFlags: ['waitingOnUserInput'],
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
logInfo('runtime', 'server_request', {
|
|
127
|
+
method: msg.method,
|
|
128
|
+
id: msg.id,
|
|
129
|
+
threadId,
|
|
130
|
+
turnId,
|
|
131
|
+
});
|
|
132
|
+
deps.emitServerRequest(msg);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleThreadStatusChanged(msg: JsonRpcMessage, deps: RuntimeMessageDispatchDeps): void {
|
|
136
|
+
const params = msg.params || {};
|
|
137
|
+
if (typeof params.threadId !== 'string') {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const status = deps.cacheThreadStatus(params.threadId, normalizeThreadStatus(params.status));
|
|
141
|
+
if (!status) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
logDebug('runtime', 'thread_status_changed', {
|
|
145
|
+
threadId: params.threadId,
|
|
146
|
+
status,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function handleTurnCompleted(msg: JsonRpcMessage, deps: RuntimeMessageDispatchDeps): void {
|
|
151
|
+
const params = msg.params || {};
|
|
152
|
+
const eventResult = buildTurnResultFromObject(
|
|
153
|
+
(items) => buildReplyFromThreadItems(items),
|
|
154
|
+
params.turn,
|
|
155
|
+
typeof params.threadId === 'string' ? params.threadId : null,
|
|
156
|
+
null,
|
|
157
|
+
);
|
|
158
|
+
logInfo('runtime', 'turn_completed_event', {
|
|
159
|
+
turnId: eventResult?.turnId || params.turn?.id,
|
|
160
|
+
status: eventResult?.status || params.turn?.status || 'completed',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
if (!eventResult || !deps.turnStates.has(eventResult.turnId)) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const turn = deps.turnStates.get(eventResult.turnId);
|
|
168
|
+
if (!turn) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
turn.completedEventResult = mergeTerminalTurnResult(eventResult, turn.completedEventResult || eventResult);
|
|
172
|
+
const resolved = turn.completedEventResult;
|
|
173
|
+
if (resolved.status !== 'completed' || Boolean(resolved.reply)) {
|
|
174
|
+
deps.resolveTurnResult(eventResult.turnId, resolved);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ApprovalKind,
|
|
3
|
+
ApprovalRequest,
|
|
4
|
+
RuntimeErrorDetails,
|
|
5
|
+
RuntimeProgressMeta,
|
|
6
|
+
RuntimeThreadActiveFlag,
|
|
7
|
+
RuntimeThreadStatus,
|
|
8
|
+
ServerRequestResolvedEvent,
|
|
9
|
+
UserInputOption,
|
|
10
|
+
UserInputQuestion,
|
|
11
|
+
UserInputRequest,
|
|
12
|
+
} from './types.ts';
|
|
13
|
+
|
|
14
|
+
export type AgentMessagePhase = 'commentary' | 'final_answer' | 'unknown';
|
|
15
|
+
|
|
16
|
+
export interface JsonRpcMessage {
|
|
17
|
+
id?: string | number;
|
|
18
|
+
method?: string;
|
|
19
|
+
params?: any;
|
|
20
|
+
result?: any;
|
|
21
|
+
error?: any;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type ApprovalPolicy = 'untrusted' | 'on-failure' | 'on-request' | 'never';
|
|
25
|
+
export type SandboxMode = 'read-only' | 'workspace-write' | 'danger-full-access';
|
|
26
|
+
|
|
27
|
+
export type ReadOnlyAccess =
|
|
28
|
+
| { type: 'restricted'; includePlatformDefaults?: boolean; readableRoots?: string[] }
|
|
29
|
+
| { type: 'fullAccess' };
|
|
30
|
+
|
|
31
|
+
export type SandboxPolicy =
|
|
32
|
+
| { type: 'readOnly'; networkAccess?: boolean; access?: ReadOnlyAccess }
|
|
33
|
+
| { type: 'workspaceWrite'; networkAccess?: boolean; readOnlyAccess?: ReadOnlyAccess; writableRoots?: string[]; excludeSlashTmp?: boolean; excludeTmpdirEnvVar?: boolean }
|
|
34
|
+
| { type: 'dangerFullAccess' }
|
|
35
|
+
| { type: 'externalSandbox'; networkAccess?: 'restricted' | 'enabled' };
|
|
36
|
+
|
|
37
|
+
export interface ThreadOptions {
|
|
38
|
+
approvalPolicy?: ApprovalPolicy | null;
|
|
39
|
+
sandbox?: SandboxMode | null;
|
|
40
|
+
developerInstructions?: string | null;
|
|
41
|
+
baseInstructions?: string | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RunTurnOptions {
|
|
45
|
+
threadId: string;
|
|
46
|
+
prompt: string;
|
|
47
|
+
messageId: string;
|
|
48
|
+
cwd: string;
|
|
49
|
+
onTurnStarted?: (turnId: string) => Promise<void> | void;
|
|
50
|
+
onProgress?: (text: string, meta?: RuntimeProgressMeta) => Promise<void> | void;
|
|
51
|
+
onThreadStatus?: (status: RuntimeThreadStatus, meta?: { threadId?: string | null; turnId?: string | null }) => Promise<void> | void;
|
|
52
|
+
onApproval?: (approval: ApprovalRequest) => Promise<void> | void;
|
|
53
|
+
onUserInput?: (request: UserInputRequest) => Promise<void> | void;
|
|
54
|
+
approvalPolicy?: ApprovalPolicy | null;
|
|
55
|
+
sandboxPolicy?: SandboxPolicy | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const THREAD_ACTIVE_FLAGS = new Set<RuntimeThreadActiveFlag>(['waitingOnApproval', 'waitingOnUserInput']);
|
|
59
|
+
|
|
60
|
+
export function approvalIdFor(msg: JsonRpcMessage): string {
|
|
61
|
+
const params = msg.params || {};
|
|
62
|
+
return String(params.approvalId || msg.id);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function approvalKindFor(method?: string): ApprovalKind | null {
|
|
66
|
+
if (method === 'item/commandExecution/requestApproval') {
|
|
67
|
+
return 'command';
|
|
68
|
+
}
|
|
69
|
+
if (method === 'item/fileChange/requestApproval') {
|
|
70
|
+
return 'file_change';
|
|
71
|
+
}
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function approvalRequestFrom(msg: JsonRpcMessage, messageId: string): ApprovalRequest | null {
|
|
76
|
+
const kind = approvalKindFor(msg.method);
|
|
77
|
+
if (!kind || msg.id == null) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const params = msg.params || {};
|
|
81
|
+
return {
|
|
82
|
+
approvalId: approvalIdFor(msg),
|
|
83
|
+
kind,
|
|
84
|
+
requestId: msg.id,
|
|
85
|
+
threadId: params.threadId,
|
|
86
|
+
turnId: params.turnId,
|
|
87
|
+
messageId,
|
|
88
|
+
command: params.command || null,
|
|
89
|
+
reason: params.reason || null,
|
|
90
|
+
grantRoot: params.grantRoot || null,
|
|
91
|
+
cwd: params.cwd || null,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function normalizeThreadStatus(status: any): RuntimeThreadStatus | null {
|
|
96
|
+
if (!status || typeof status !== 'object') {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const type = typeof status.type === 'string' ? status.type : null;
|
|
100
|
+
if (type !== 'notLoaded' && type !== 'idle' && type !== 'systemError' && type !== 'active') {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const activeFlags = Array.isArray(status.activeFlags)
|
|
104
|
+
? status.activeFlags.filter((flag: unknown): flag is RuntimeThreadActiveFlag => typeof flag === 'string' && THREAD_ACTIVE_FLAGS.has(flag as RuntimeThreadActiveFlag))
|
|
105
|
+
: [];
|
|
106
|
+
return { type, activeFlags };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function normalizeAgentMessagePhase(value: unknown): AgentMessagePhase {
|
|
110
|
+
if (value === 'commentary' || value === 'final_answer') {
|
|
111
|
+
return value;
|
|
112
|
+
}
|
|
113
|
+
return 'unknown';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function isContextCompactionType(value: unknown): boolean {
|
|
117
|
+
if (typeof value !== 'string') {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return value === 'contextCompaction' || value === 'context_compaction';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function normalizeUserInputOptions(input: unknown): UserInputOption[] | null {
|
|
124
|
+
if (!Array.isArray(input)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const options = input
|
|
128
|
+
.filter((item) => item && typeof item === 'object')
|
|
129
|
+
.map((item: any) => ({
|
|
130
|
+
label: typeof item.label === 'string' ? item.label : '',
|
|
131
|
+
description: typeof item.description === 'string' ? item.description : '',
|
|
132
|
+
}))
|
|
133
|
+
.filter((item) => item.label);
|
|
134
|
+
return options.length > 0 ? options : null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function normalizeUserInputQuestion(input: unknown): UserInputQuestion | null {
|
|
138
|
+
if (!input || typeof input !== 'object') {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const raw = input as Record<string, unknown>;
|
|
142
|
+
if (typeof raw.id !== 'string' || typeof raw.header !== 'string' || typeof raw.question !== 'string') {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
id: raw.id,
|
|
147
|
+
header: raw.header,
|
|
148
|
+
question: raw.question,
|
|
149
|
+
isOther: raw.isOther === true,
|
|
150
|
+
isSecret: raw.isSecret === true,
|
|
151
|
+
options: normalizeUserInputOptions(raw.options),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function normalizeSchemaObject(input: unknown): Record<string, unknown> | null {
|
|
156
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
return input as Record<string, unknown>;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function appendQuestionOption(
|
|
163
|
+
options: UserInputOption[],
|
|
164
|
+
seen: Set<string>,
|
|
165
|
+
label: string | null,
|
|
166
|
+
description: string | null = null,
|
|
167
|
+
): void {
|
|
168
|
+
const normalizedLabel = (label || '').trim();
|
|
169
|
+
if (!normalizedLabel || seen.has(normalizedLabel)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
seen.add(normalizedLabel);
|
|
173
|
+
options.push({
|
|
174
|
+
label: normalizedLabel,
|
|
175
|
+
description: (description || '').trim(),
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function normalizeQuestionOptionsFromSchema(schema: Record<string, unknown> | null): UserInputOption[] | null {
|
|
180
|
+
if (!schema) {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const options: UserInputOption[] = [];
|
|
185
|
+
const seen = new Set<string>();
|
|
186
|
+
const schemaDescription = typeof schema.description === 'string' ? schema.description : null;
|
|
187
|
+
|
|
188
|
+
if (Array.isArray(schema.enum)) {
|
|
189
|
+
for (const value of schema.enum) {
|
|
190
|
+
appendQuestionOption(options, seen, String(value), schemaDescription);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for (const key of ['oneOf', 'anyOf']) {
|
|
195
|
+
const variants = schema[key];
|
|
196
|
+
if (!Array.isArray(variants)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
for (const variant of variants) {
|
|
200
|
+
const raw = normalizeSchemaObject(variant);
|
|
201
|
+
if (!raw) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const variantDescription = typeof raw.description === 'string' ? raw.description : schemaDescription;
|
|
205
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'const')) {
|
|
206
|
+
appendQuestionOption(
|
|
207
|
+
options,
|
|
208
|
+
seen,
|
|
209
|
+
typeof raw.title === 'string' ? raw.title : String(raw.const),
|
|
210
|
+
variantDescription,
|
|
211
|
+
);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
if (Array.isArray(raw.enum) && raw.enum.length === 1) {
|
|
215
|
+
const value = raw.enum[0];
|
|
216
|
+
appendQuestionOption(
|
|
217
|
+
options,
|
|
218
|
+
seen,
|
|
219
|
+
typeof raw.title === 'string' ? raw.title : String(value),
|
|
220
|
+
variantDescription,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return options.length > 0 ? options : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function questionPromptFromSchema(message: string, schema: Record<string, unknown> | null): string {
|
|
230
|
+
const prompt = message.trim();
|
|
231
|
+
const description = typeof schema?.description === 'string' ? schema.description.trim() : '';
|
|
232
|
+
if (description && description !== prompt) {
|
|
233
|
+
return prompt + '\n' + description;
|
|
234
|
+
}
|
|
235
|
+
return prompt;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function runtimeErrorMessage(error: unknown): string {
|
|
239
|
+
if (error instanceof Error) {
|
|
240
|
+
return error.message || 'runtime request failed';
|
|
241
|
+
}
|
|
242
|
+
if (typeof error === 'string') {
|
|
243
|
+
return error.trim() || 'runtime request failed';
|
|
244
|
+
}
|
|
245
|
+
if (error && typeof error === 'object') {
|
|
246
|
+
const raw = error as { message?: unknown; code?: unknown; data?: unknown };
|
|
247
|
+
const message = typeof raw.message === 'string' ? raw.message.trim() : '';
|
|
248
|
+
const code = raw.code === undefined || raw.code === null || raw.code === '' ? '' : String(raw.code).trim();
|
|
249
|
+
if (message && code) {
|
|
250
|
+
return `${message} (code: ${code})`;
|
|
251
|
+
}
|
|
252
|
+
if (message) {
|
|
253
|
+
return message;
|
|
254
|
+
}
|
|
255
|
+
if (code) {
|
|
256
|
+
return `runtime request failed (code: ${code})`;
|
|
257
|
+
}
|
|
258
|
+
if (raw.data && typeof raw.data === 'object' && typeof (raw.data as { message?: unknown }).message === 'string') {
|
|
259
|
+
return String((raw.data as { message: string }).message).trim();
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return 'runtime request failed';
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function normalizeRuntimeError(error: unknown): Error {
|
|
266
|
+
if (error instanceof Error) {
|
|
267
|
+
return error;
|
|
268
|
+
}
|
|
269
|
+
const normalized = new Error(runtimeErrorMessage(error)) as RuntimeErrorDetails;
|
|
270
|
+
if (error && typeof error === 'object') {
|
|
271
|
+
const raw = error as { code?: unknown; data?: unknown };
|
|
272
|
+
if (raw.code !== undefined && raw.code !== null && raw.code !== '') {
|
|
273
|
+
normalized.code = typeof raw.code === 'number' ? raw.code : String(raw.code).trim();
|
|
274
|
+
}
|
|
275
|
+
if (Object.prototype.hasOwnProperty.call(raw, 'data')) {
|
|
276
|
+
normalized.data = raw.data;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return normalized;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function elicitationQuestionsFrom(params: Record<string, unknown>): UserInputQuestion[] {
|
|
283
|
+
const message = typeof params.message === 'string' ? params.message : '需要你确认后我才能继续。';
|
|
284
|
+
const mode = params.mode === 'url' ? 'url' : 'form';
|
|
285
|
+
|
|
286
|
+
if (mode === 'url') {
|
|
287
|
+
const url = typeof params.url === 'string' ? params.url : '';
|
|
288
|
+
const question = url ? message + '\n' + url : message;
|
|
289
|
+
return [
|
|
290
|
+
{
|
|
291
|
+
id: 'response',
|
|
292
|
+
header: 'MCP 确认',
|
|
293
|
+
question,
|
|
294
|
+
options: [
|
|
295
|
+
{ label: '同意', description: '继续执行' },
|
|
296
|
+
{ label: '拒绝', description: '拒绝本次请求' },
|
|
297
|
+
{ label: '取消', description: '取消当前请求' },
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
];
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const requestedSchema = normalizeSchemaObject(params.requestedSchema);
|
|
304
|
+
const properties = normalizeSchemaObject(requestedSchema?.properties);
|
|
305
|
+
const rootLooksLikeObject = requestedSchema?.type === 'object' || Boolean(properties && Object.keys(properties).length > 0);
|
|
306
|
+
|
|
307
|
+
if (rootLooksLikeObject && properties) {
|
|
308
|
+
const questions = Object.entries(properties)
|
|
309
|
+
.map(([id, value]) => {
|
|
310
|
+
const schema = normalizeSchemaObject(value);
|
|
311
|
+
return {
|
|
312
|
+
id,
|
|
313
|
+
header: typeof schema?.title === 'string' ? schema.title : id,
|
|
314
|
+
question: questionPromptFromSchema(message, schema),
|
|
315
|
+
isSecret: schema?.format === 'password',
|
|
316
|
+
options: normalizeQuestionOptionsFromSchema(schema),
|
|
317
|
+
} satisfies UserInputQuestion;
|
|
318
|
+
})
|
|
319
|
+
.filter((question) => Boolean(question.id));
|
|
320
|
+
|
|
321
|
+
if (questions.length > 0) {
|
|
322
|
+
return questions;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return [
|
|
327
|
+
{
|
|
328
|
+
id: 'response',
|
|
329
|
+
header: 'MCP 确认',
|
|
330
|
+
question: questionPromptFromSchema(message, requestedSchema),
|
|
331
|
+
options: normalizeQuestionOptionsFromSchema(requestedSchema),
|
|
332
|
+
},
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function userInputRequestFrom(msg: JsonRpcMessage, messageId: string): UserInputRequest | null {
|
|
337
|
+
if (msg.method !== 'item/tool/requestUserInput' || msg.id == null) {
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
const params = msg.params || {};
|
|
341
|
+
if (typeof params.threadId !== 'string' || typeof params.turnId !== 'string' || !Array.isArray(params.questions)) {
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
const questions = params.questions
|
|
345
|
+
.map((question: unknown) => normalizeUserInputQuestion(question))
|
|
346
|
+
.filter((question: UserInputQuestion | null): question is UserInputQuestion => Boolean(question));
|
|
347
|
+
if (questions.length === 0) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return {
|
|
351
|
+
requestId: msg.id,
|
|
352
|
+
threadId: params.threadId,
|
|
353
|
+
turnId: params.turnId,
|
|
354
|
+
itemId: typeof params.itemId === 'string' ? params.itemId : null,
|
|
355
|
+
messageId,
|
|
356
|
+
kind: 'questionnaire',
|
|
357
|
+
questions,
|
|
358
|
+
elicitation: null,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function serverRequestResolvedEventFrom(msg: JsonRpcMessage): ServerRequestResolvedEvent | null {
|
|
363
|
+
if (msg.method !== 'serverRequest/resolved') {
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
const params = msg.params || {};
|
|
367
|
+
const requestId = params.requestId ?? params.id ?? params.serverRequestId ?? params.resolvedRequestId ?? null;
|
|
368
|
+
if (requestId == null) {
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
requestId,
|
|
373
|
+
threadId: typeof params.threadId === 'string' ? params.threadId : null,
|
|
374
|
+
turnId: typeof params.turnId === 'string' ? params.turnId : null,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export function elicitationRequestFrom(msg: JsonRpcMessage, messageId: string): UserInputRequest | null {
|
|
379
|
+
if (msg.method !== 'mcpServer/elicitation/request' || msg.id == null) {
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
const params = msg.params || {};
|
|
383
|
+
if (typeof params.threadId !== 'string' || typeof params.serverName !== 'string' || typeof params.message !== 'string') {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const mode = params.mode === 'url' ? 'url' : params.mode === 'form' ? 'form' : null;
|
|
387
|
+
if (!mode) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
if (mode === 'url' && typeof params.url !== 'string') {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
requestId: msg.id,
|
|
396
|
+
threadId: params.threadId,
|
|
397
|
+
turnId: typeof params.turnId === 'string' ? params.turnId : '',
|
|
398
|
+
itemId: null,
|
|
399
|
+
messageId,
|
|
400
|
+
kind: 'mcp_elicitation',
|
|
401
|
+
questions: elicitationQuestionsFrom(params),
|
|
402
|
+
elicitation: {
|
|
403
|
+
serverName: params.serverName,
|
|
404
|
+
mode,
|
|
405
|
+
message: params.message,
|
|
406
|
+
requestedSchema: mode === 'form' ? params.requestedSchema : undefined,
|
|
407
|
+
url: mode === 'url' ? params.url : null,
|
|
408
|
+
elicitationId: mode === 'url' && typeof params.elicitationId === 'string' ? params.elicitationId : null,
|
|
409
|
+
},
|
|
410
|
+
};
|
|
411
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { logDebug } from './logger.ts';
|
|
2
|
+
import type { PendingRuntimeRequest } from './runtime-client-message-dispatch.ts';
|
|
3
|
+
|
|
4
|
+
const REQUEST_TIMEOUT_MS = Number(process.env.WORK_ALLY_CODEX_REQUEST_TIMEOUT_MS || '15000');
|
|
5
|
+
const TURN_START_REQUEST_TIMEOUT_MS = Number(process.env.WORK_ALLY_CODEX_TURN_START_TIMEOUT_MS || '60000');
|
|
6
|
+
const THREAD_REQUEST_TIMEOUT_MS = Number(process.env.WORK_ALLY_CODEX_THREAD_REQUEST_TIMEOUT_MS || '30000');
|
|
7
|
+
|
|
8
|
+
interface RuntimeRequestDeps {
|
|
9
|
+
connect(): Promise<void>;
|
|
10
|
+
createId(): string;
|
|
11
|
+
pending: Map<string | number, PendingRuntimeRequest>;
|
|
12
|
+
sendRaw(payload: unknown): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function requestTimeoutForMethod(method: string): number {
|
|
16
|
+
switch (method) {
|
|
17
|
+
case 'thread/start':
|
|
18
|
+
case 'thread/resume':
|
|
19
|
+
return THREAD_REQUEST_TIMEOUT_MS;
|
|
20
|
+
case 'turn/start':
|
|
21
|
+
return TURN_START_REQUEST_TIMEOUT_MS;
|
|
22
|
+
default:
|
|
23
|
+
return REQUEST_TIMEOUT_MS;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function rejectAllPendingRequests(
|
|
28
|
+
pending: Map<string | number, PendingRuntimeRequest>,
|
|
29
|
+
error: Error,
|
|
30
|
+
): void {
|
|
31
|
+
for (const [id, request] of pending.entries()) {
|
|
32
|
+
clearTimeout(request.timer);
|
|
33
|
+
request.reject(error);
|
|
34
|
+
pending.delete(id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function sendRuntimeRequest(
|
|
39
|
+
deps: RuntimeRequestDeps,
|
|
40
|
+
method: string,
|
|
41
|
+
params: unknown,
|
|
42
|
+
): Promise<any> {
|
|
43
|
+
await deps.connect();
|
|
44
|
+
const id = deps.createId();
|
|
45
|
+
const timeoutMs = requestTimeoutForMethod(method);
|
|
46
|
+
const promise = new Promise((resolve, reject) => {
|
|
47
|
+
const timer = setTimeout(() => {
|
|
48
|
+
deps.pending.delete(id);
|
|
49
|
+
reject(new Error(`runtime request timed out: ${method}`));
|
|
50
|
+
}, timeoutMs);
|
|
51
|
+
deps.pending.set(id, { resolve, reject, timer });
|
|
52
|
+
});
|
|
53
|
+
logDebug('runtime', 'request_sent', { id, method });
|
|
54
|
+
deps.sendRaw({ jsonrpc: '2.0', id, method, params });
|
|
55
|
+
return promise;
|
|
56
|
+
}
|