create-windy 0.8.2 → 0.8.3

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.
Files changed (86) hide show
  1. package/dist/cli.js +18 -1
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/server/src/agent/admin-catalog-bootstrap.ts +25 -0
  5. package/template/apps/server/src/agent/agent-session-bootstrap.ts +7 -8
  6. package/template/apps/server/src/agent/agent-session-facade.ts +12 -0
  7. package/template/apps/server/src/agent/agent-session-validation.ts +43 -0
  8. package/template/apps/server/src/agent/operation-catalog.test.ts +202 -0
  9. package/template/apps/server/src/agent/operation-catalog.ts +135 -0
  10. package/template/apps/server/src/agent/run-browser-visibility.test.ts +59 -0
  11. package/template/apps/server/src/agent/run-browser-visibility.ts +11 -0
  12. package/template/apps/server/src/agent/run-operation-policy.ts +1 -0
  13. package/template/apps/server/src/agent/run-route-response.ts +35 -0
  14. package/template/apps/server/src/agent/run-routes.ts +46 -43
  15. package/template/apps/server/src/agent/runtime-bootstrap.ts +21 -20
  16. package/template/apps/server/src/agent/runtime-disabled-composition.ts +28 -0
  17. package/template/apps/server/src/agent/session-projection-facade.ts +8 -9
  18. package/template/apps/server/src/agent/session-projection-governance.ts +10 -0
  19. package/template/apps/server/src/agent/session-projection-view.ts +19 -16
  20. package/template/apps/server/src/agent/session-recording-reader.contract.test.ts +131 -0
  21. package/template/apps/server/src/agent/session-recording-reader.ts +62 -0
  22. package/template/apps/server/src/agent/tool-catalog-audit.ts +22 -0
  23. package/template/apps/server/src/agent/tool-catalog-availability.ts +21 -0
  24. package/template/apps/server/src/agent/tool-catalog-routes.test.ts +103 -0
  25. package/template/apps/server/src/agent/tool-catalog-routes.ts +62 -0
  26. package/template/apps/server/src/agent/tool-catalog.test.ts +122 -0
  27. package/template/apps/server/src/agent/tool-catalog.ts +44 -0
  28. package/template/apps/server/src/agent/tool-registry.ts +45 -14
  29. package/template/apps/server/src/agent/workbench-bootstrap.ts +39 -0
  30. package/template/apps/server/src/agent/workbench-event-projection.ts +115 -0
  31. package/template/apps/server/src/agent/workbench-facade.contract.test.ts +189 -0
  32. package/template/apps/server/src/agent/workbench-facade.ts +339 -0
  33. package/template/apps/server/src/agent/workbench-recording.contract.test.ts +328 -0
  34. package/template/apps/server/src/agent/workbench-routes.ts +97 -0
  35. package/template/apps/server/src/agent/workbench-run-integration.test.ts +231 -0
  36. package/template/apps/server/src/agent/workbench-run-reader.ts +187 -0
  37. package/template/apps/server/src/agent/workbench-safe-result.ts +61 -0
  38. package/template/apps/server/src/agent/workbench-sse.ts +50 -0
  39. package/template/apps/server/src/agent-workflow/query-service.ts +36 -0
  40. package/template/apps/server/src/agent-workflow/recording-projection.contract.test.ts +196 -0
  41. package/template/apps/server/src/agent-workflow/recording-projection.ts +86 -0
  42. package/template/apps/web/src/pages/system/AgentDebugPage.vue +14 -0
  43. package/template/apps/web/src/pages/system/AgentSessionPage.vue +29 -8
  44. package/template/apps/web/src/pages/system/AgentToolsPage.vue +76 -0
  45. package/template/apps/web/src/pages/system/components/agent-workbench/AgentWorkbench.vue +236 -0
  46. package/template/apps/web/src/pages/system/components/agent-workbench/DraftLeaveDialog.vue +34 -0
  47. package/template/apps/web/src/pages/system/components/agent-workbench/ReplayEventDetail.vue +175 -0
  48. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.vue +142 -0
  49. package/template/apps/web/src/pages/system/components/agent-workbench/SessionReplayTimeline.webtest.ts +242 -0
  50. package/template/apps/web/src/pages/system/components/agent-workbench/TestRunComposer.vue +97 -0
  51. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.vue +100 -0
  52. package/template/apps/web/src/pages/system/components/agent-workbench/ToolCatalogPanel.webtest.ts +55 -0
  53. package/template/apps/web/src/pages/system/components/agent-workbench/ToolRuleDetail.vue +81 -0
  54. package/template/apps/web/src/pages/system/components/agent-workbench/WorkbenchSessionList.vue +57 -0
  55. package/template/apps/web/src/pages/system/components/agent-workbench/catalog-presenter.ts +20 -0
  56. package/template/apps/web/src/pages/system/components/agent-workbench/replay-presenter.ts +94 -0
  57. package/template/apps/web/src/pages/system/composables/useAgentWorkbench.ts +298 -0
  58. package/template/apps/web/src/pages/system/composables/useAgentWorkbench.webtest.ts +164 -0
  59. package/template/apps/web/src/pages/system/composables/useSessionReplay.ts +48 -0
  60. package/template/apps/web/src/router/index.ts +4 -0
  61. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  62. package/template/apps/web/src/services/agent-admin-api.ts +155 -0
  63. package/template/apps/web/src/services/agent-admin-api.webtest.ts +120 -0
  64. package/template/apps/web/src/services/agent-workbench-event.ts +60 -0
  65. package/template/docs/architecture/agent-harness.md +4 -0
  66. package/template/packages/modules/src/ai-operation-composition.ts +9 -0
  67. package/template/packages/modules/src/ai-operation-visibility.test.ts +33 -0
  68. package/template/packages/modules/src/manifest.ts +2 -0
  69. package/template/packages/modules/src/system-admin-routes.ts +16 -0
  70. package/template/packages/modules/src/system-admin-ui-catalog.ts +7 -1
  71. package/template/packages/modules/src/system-agent-admin-bindings.ts +42 -0
  72. package/template/packages/modules/src/system-agent-admin-menus.ts +26 -0
  73. package/template/packages/modules/src/system-agent-admin-registration.test.ts +52 -0
  74. package/template/packages/modules/src/system-agent-audit-actions.ts +5 -0
  75. package/template/packages/modules/src/system-ai-api-bindings.ts +2 -0
  76. package/template/packages/modules/src/system-audit-actions.ts +25 -0
  77. package/template/packages/modules/src/system-features.ts +19 -0
  78. package/template/packages/modules/src/system-module-catalog.ts +14 -12
  79. package/template/packages/modules/src/system-module-ownership.ts +12 -0
  80. package/template/packages/modules/src/system-modules.ts +2 -0
  81. package/template/packages/modules/src/system-permissions.ts +5 -0
  82. package/template/packages/server-sdk/index.ts +2 -0
  83. package/template/packages/server-sdk/src/agent-session-provider.ts +19 -0
  84. package/template/packages/shared/index.ts +2 -0
  85. package/template/packages/shared/src/agent-admin-catalog.ts +73 -0
  86. package/template/packages/shared/src/agent-workbench.ts +100 -0
@@ -1,3 +1,9 @@
1
+ import {
2
+ facadeError,
3
+ errorResponse,
4
+ required,
5
+ isRecord,
6
+ } from "./run-route-response.js";
1
7
  import { timingSafeEqual } from "node:crypto";
2
8
  import type { RequestGuardContext } from "../guards.js";
3
9
  import type { RouteApp } from "../system/routes.js";
@@ -7,6 +13,8 @@ import type { AgentRuntimePort } from "./run-contracts.js";
7
13
  import type { AgentRunSessionLinkPort } from "./session-run-link.js";
8
14
  import type { SessionRunResultMemoryPort } from "./session-result-memory.js";
9
15
  import type { ModuleAiJsonValue } from "@southwind-ai/server-sdk";
16
+ import { projectPublicRunEvents } from "./workbench-sse.js";
17
+ import { assertBrowserVisibleOperation } from "./run-browser-visibility.js";
10
18
  import {
11
19
  AgentToolHostError,
12
20
  attachExecutionGrant,
@@ -46,6 +54,7 @@ export function registerAgentRunRoutes(
46
54
  const context = raw as RouteContext;
47
55
  try {
48
56
  const request = parseStartRequest(context.body);
57
+ assertBrowserVisibleOperation(facade, request.operationKey);
49
58
  const idempotencyKey = context.request.headers.get("idempotency-key");
50
59
  if (!idempotencyKey?.trim()) {
51
60
  return errorResponse("INVALID_INPUT", "幂等键不能为空", 400);
@@ -70,12 +79,15 @@ export function registerAgentRunRoutes(
70
79
  try {
71
80
  const runId = required(context.params.runId);
72
81
  const operationKey = parseOperationKey(context.query.operationKey);
82
+ assertBrowserVisibleOperation(facade, operationKey);
73
83
  await facade.authorizeRun(operationKey, runId, context.guardContext);
74
84
  const cursor = parseCursor(
75
85
  context.query.after,
76
86
  context.request.headers.get("last-event-id"),
77
87
  );
78
- return await remote.events(runId, cursor, context.request.signal);
88
+ return projectPublicRunEvents(
89
+ await remote.events(runId, cursor, context.request.signal),
90
+ );
79
91
  } catch (error) {
80
92
  return facadeError(error);
81
93
  }
@@ -86,6 +98,7 @@ export function registerAgentRunRoutes(
86
98
  try {
87
99
  const runId = required(context.params.runId);
88
100
  const operationKey = parseOperationKey(context.query.operationKey);
101
+ assertBrowserVisibleOperation(facade, operationKey);
89
102
  const sessionId = context.query.sessionId
90
103
  ? parseSessionId(context.query.sessionId)
91
104
  : undefined;
@@ -105,8 +118,14 @@ export function registerAgentRunRoutes(
105
118
  status.status === "cancelled"
106
119
  ) {
107
120
  if (status.status === "completed") {
108
- await projectResult(resultMemory, boundSessionId, runId,
109
- status.lastSequence, response, context);
121
+ await projectResult(
122
+ resultMemory,
123
+ boundSessionId,
124
+ runId,
125
+ status.lastSequence,
126
+ response,
127
+ context,
128
+ );
110
129
  }
111
130
  await completeSessionRun(
112
131
  sessionLinks,
@@ -128,6 +147,7 @@ export function registerAgentRunRoutes(
128
147
  try {
129
148
  const runId = required(context.params.runId);
130
149
  const operationKey = parseOperationKey(context.query.operationKey);
150
+ assertBrowserVisibleOperation(facade, operationKey);
131
151
  const sessionId = context.query.sessionId
132
152
  ? parseSessionId(context.query.sessionId)
133
153
  : undefined;
@@ -141,7 +161,11 @@ export function registerAgentRunRoutes(
141
161
  isRecord(context.body) && typeof context.body.reason === "string"
142
162
  ? context.body.reason.slice(0, 200)
143
163
  : undefined;
144
- const response = await remote.cancel(runId, reason, context.request.signal);
164
+ const response = await remote.cancel(
165
+ runId,
166
+ reason,
167
+ context.request.signal,
168
+ );
145
169
  if (authorized.sessionId && response.ok) {
146
170
  const status = await remote.status(runId, context.request.signal);
147
171
  if (!["completed", "failed", "cancelled"].includes(status.status)) {
@@ -149,8 +173,14 @@ export function registerAgentRunRoutes(
149
173
  }
150
174
  if (status.status === "completed") {
151
175
  const result = await remote.result(runId, context.request.signal);
152
- await projectResult(resultMemory, authorized.sessionId, runId,
153
- status.lastSequence, result, context);
176
+ await projectResult(
177
+ resultMemory,
178
+ authorized.sessionId,
179
+ runId,
180
+ status.lastSequence,
181
+ result,
182
+ context,
183
+ );
154
184
  }
155
185
  await completeSessionRun(
156
186
  sessionLinks,
@@ -171,6 +201,7 @@ export function registerAgentRunRoutes(
171
201
  try {
172
202
  const runId = required(context.params.runId);
173
203
  const request = parseAttachRequest(context.body);
204
+ assertBrowserVisibleOperation(facade, request.operationKey);
174
205
  const authorized = await facade.authorizeRunMutation(
175
206
  request.operationKey,
176
207
  runId,
@@ -188,8 +219,14 @@ export function registerAgentRunRoutes(
188
219
  if (authorized.sessionId && attached.terminalEvent) {
189
220
  if (attached.status === "completed") {
190
221
  const response = await remote.result(runId, context.request.signal);
191
- await projectResult(resultMemory, authorized.sessionId, runId,
192
- attached.terminalEvent.sequence, response, context);
222
+ await projectResult(
223
+ resultMemory,
224
+ authorized.sessionId,
225
+ runId,
226
+ attached.terminalEvent.sequence,
227
+ response,
228
+ context,
229
+ );
193
230
  }
194
231
  await completeSessionRun(
195
232
  sessionLinks,
@@ -267,7 +304,7 @@ async function projectResult(
267
304
  sessionId,
268
305
  runId,
269
306
  terminalSequence,
270
- result: await response.clone().json() as ModuleAiJsonValue,
307
+ result: (await response.clone().json()) as ModuleAiJsonValue,
271
308
  context: context.guardContext,
272
309
  signal: context.request.signal,
273
310
  });
@@ -302,37 +339,3 @@ export function validInternalToken(
302
339
  const right = Buffer.from(expected);
303
340
  return left.length === right.length && timingSafeEqual(left, right);
304
341
  }
305
-
306
- function facadeError(error: unknown): Response {
307
- if (error instanceof AgentRunFacadeError) {
308
- return errorResponse(error.code, error.message, error.status);
309
- }
310
- return errorResponse(
311
- "RUNTIME_UNAVAILABLE",
312
- "Agent Runtime 暂不可用",
313
- 503,
314
- true,
315
- );
316
- }
317
-
318
- function errorResponse(
319
- code: string,
320
- message: string,
321
- status: number,
322
- retryable = false,
323
- ): Response {
324
- return Response.json(
325
- { code, message, requestId: crypto.randomUUID(), retryable },
326
- { status },
327
- );
328
- }
329
-
330
- function required(value: string | undefined): string {
331
- if (!value)
332
- throw new AgentRunFacadeError("INVALID_INPUT", "Run ID 无效", 400);
333
- return value;
334
- }
335
-
336
- function isRecord(value: unknown): value is Record<string, unknown> {
337
- return Boolean(value && typeof value === "object" && !Array.isArray(value));
338
- }
@@ -1,4 +1,6 @@
1
1
  import { resolveCurrentSystemActor } from "../system/actor.js";
2
+ import type { AgentAdminReadinessRef } from "./admin-catalog-bootstrap.js";
3
+ import { registerAgentAdminCatalogRuntime } from "./admin-catalog-bootstrap.js";
2
4
  import { AiArtifactCodec } from "../ai-artifact/codec.js";
3
5
  import { registerAiArtifactRoutes } from "../ai-artifact/routes.js";
4
6
  import { AiArtifactRuntime } from "../ai-artifact/runtime.js";
@@ -41,7 +43,7 @@ import { InMemoryAgentRunBindingRepository } from "./run-binding-repository.js";
41
43
  import { AgentBackgroundOperationExecutor } from "./background-operation.js";
42
44
  import { createModuleAiPorts } from "./module-ai-port-factory.js";
43
45
  import { createManifestAuthorizedRouteApp } from "../module-host/static-route-authorization.js";
44
- import { registerUnavailableAiRoutes } from "./unavailable-routes.js";
46
+ import { disableAgentRuntime } from "./runtime-disabled-composition.js";
45
47
  import { createClientAgentSessionRuntime } from "./client-session/bootstrap.js";
46
48
  import { attachClientAgentWorkflowArtifactRuntime } from "./client-workflow-artifact/runtime-composition.js";
47
49
  import { bindClientAgentHostRevocations } from "./client-session/host-revocation-composition.js";
@@ -52,6 +54,7 @@ import { registerScreenshotIntakeRoutes } from "./screenshot-intake-routes.js";
52
54
  import { createScreenshotIntakeRuntimeV2 } from "./screenshot-intake-bootstrap.js";
53
55
  import { requiredEnvironment } from "./required-environment.js";
54
56
  import { composeAgentLongSessionRuntime } from "./session-runtime-composition.js";
57
+ import { registerAgentWorkbenchRuntime } from "./workbench-bootstrap.js";
55
58
  import type { AgentPlatformRuntimeInput } from "./runtime-bootstrap-input.js";
56
59
  import { composeAgentToolRuntime } from "./runtime-tool-composition.js";
57
60
  import { registerProductionAgentWorkflowRuntime } from "./workflow-runtime-bootstrap.js";
@@ -62,33 +65,23 @@ import {
62
65
  export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
63
66
  const resolveAgentActor = async (actorId: string) =>
64
67
  resolveCurrentSystemActor(input.systemRepositories, actorId);
65
- const { features, toolHandlers, toolRegistry, routeAuthorizer } =
66
- composeAgentToolRuntime(input);
68
+ const tools = composeAgentToolRuntime(input);
69
+ const { features, toolHandlers, toolRegistry, routeAuthorizer } = tools;
70
+ const catalogReadiness: AgentAdminReadinessRef = {};
71
+ registerAgentAdminCatalogRuntime(input, tools, catalogReadiness);
67
72
  const aiRouteApp = createManifestAuthorizedRouteApp(
68
73
  input.app,
69
74
  routeAuthorizer,
70
75
  );
71
76
  if (!input.platformConfig.enableAI) {
72
- registerProductionAgentWorkflowRuntime(input, aiRouteApp, toolRegistry);
73
- registerUnavailableAiRoutes(
74
- createManifestAuthorizedRouteApp(input.app, routeAuthorizer),
75
- );
76
- return {
77
- toolHandlers,
78
- aiArtifacts: undefined,
79
- aiOperations: undefined,
80
- aiReadiness: undefined,
81
- clientAgentSessions: undefined,
82
- agentWorkflowProjects: undefined,
83
- sessionMemory: undefined,
84
- sourceInvalidation: undefined,
85
- invalidateAuthorization: undefined,
86
- disposeAgentRuntime: undefined,
87
- };
77
+ return disableAgentRuntime(input, aiRouteApp, tools);
88
78
  }
89
79
  const sessionSecret = requiredEnvironment("AI_ARTIFACT_ENCRYPTION_SECRET");
90
80
  const internalToken = requiredEnvironment("AGENT_SERVER_INTERNAL_TOKEN");
91
- registerAgentSessions({ ...input, secret: internalToken });
81
+ const businessSessions = registerAgentSessions({
82
+ ...input,
83
+ secret: internalToken,
84
+ });
92
85
  const aiArtifacts = new AiArtifactRuntime(
93
86
  new AiArtifactCodec(sessionSecret),
94
87
  input.hosts,
@@ -203,6 +196,7 @@ export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
203
196
  trustStore: readinessTrustStore,
204
197
  runtime: input.runtime,
205
198
  });
199
+ catalogReadiness.current = aiReadiness;
206
200
  const outputRuntime = createDocumentOutputRuntime({
207
201
  modules: input.modules,
208
202
  readiness: aiReadiness,
@@ -286,6 +280,13 @@ export function registerAgentPlatformRuntime(input: AgentPlatformRuntimeInput) {
286
280
  if (agentWorkflowRuntime?.artifacts) {
287
281
  clientWorkflowArtifacts.bindTaskArtifacts?.(agentWorkflowRuntime.artifacts);
288
282
  }
283
+ registerAgentWorkbenchRuntime(input, aiRouteApp, {
284
+ agentFacade,
285
+ remote,
286
+ business: businessSessions,
287
+ sessions: longSessions?.facade,
288
+ workflows: agentWorkflowRuntime?.query,
289
+ });
289
290
  registerScreenshotIntakeRoutes(aiRouteApp, screenshotIntake);
290
291
  registerAgentRunRoutes(
291
292
  aiRouteApp,
@@ -0,0 +1,28 @@
1
+ import type { RouteApp } from "../system/routes.js";
2
+ import type { AgentPlatformRuntimeInput } from "./runtime-bootstrap-input.js";
3
+ import type { composeAgentToolRuntime } from "./runtime-tool-composition.js";
4
+ import { registerAgentWorkbenchRuntime } from "./workbench-bootstrap.js";
5
+ import { registerProductionAgentWorkflowRuntime } from "./workflow-runtime-bootstrap.js";
6
+ import { registerUnavailableAiRoutes } from "./unavailable-routes.js";
7
+
8
+ export function disableAgentRuntime(
9
+ input: AgentPlatformRuntimeInput,
10
+ app: RouteApp,
11
+ tools: ReturnType<typeof composeAgentToolRuntime>,
12
+ ) {
13
+ registerAgentWorkbenchRuntime(input, app);
14
+ registerProductionAgentWorkflowRuntime(input, app, tools.toolRegistry);
15
+ registerUnavailableAiRoutes(app);
16
+ return {
17
+ toolHandlers: tools.toolHandlers,
18
+ aiArtifacts: undefined,
19
+ aiOperations: undefined,
20
+ aiReadiness: undefined,
21
+ clientAgentSessions: undefined,
22
+ agentWorkflowProjects: undefined,
23
+ sessionMemory: undefined,
24
+ sourceInvalidation: undefined,
25
+ invalidateAuthorization: undefined,
26
+ disposeAgentRuntime: undefined,
27
+ };
28
+ }
@@ -1,4 +1,5 @@
1
- import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
1
+ import { projectSessionRecording } from "./session-recording-reader.js";
2
+ import { sessionProjectionGovernance as governance } from "./session-projection-governance.js";
2
3
  import type { RequestGuardContext } from "../guards.js";
3
4
  import type { MemoryObjectAuthorizationPort } from "./context-memory/contracts.js";
4
5
  import {
@@ -89,6 +90,12 @@ export class AgentSessionProjectionFacade {
89
90
  };
90
91
  }
91
92
 
93
+ async recording(sessionId: string, context: RequestGuardContext) {
94
+ return projectSessionRecording(
95
+ await this.replay(sessionId, undefined, context),
96
+ );
97
+ }
98
+
92
99
  async contextVersions(sessionId: string, context: RequestGuardContext) {
93
100
  const session = await this.requireSession(sessionId, context);
94
101
  const rows = await this.contexts.listForSession(session.sessionId);
@@ -339,11 +346,3 @@ export class AgentSessionProjectionFacade {
339
346
  return session;
340
347
  }
341
348
  }
342
-
343
- function governance(context: RequestGuardContext) {
344
- return {
345
- actorId: context.actor.id,
346
- scopeDigest:
347
- `sha256:${captureAuthorizationScope(context.actor.scopeContext).hash}` as const,
348
- };
349
- }
@@ -0,0 +1,10 @@
1
+ import { captureAuthorizationScope } from "../data-access/scope-snapshot.js";
2
+ import type { RequestGuardContext } from "../guards.js";
3
+
4
+ export function sessionProjectionGovernance(context: RequestGuardContext) {
5
+ return {
6
+ actorId: context.actor.id,
7
+ scopeDigest:
8
+ `sha256:${captureAuthorizationScope(context.actor.scopeContext).hash}` as const,
9
+ };
10
+ }
@@ -27,9 +27,7 @@ export function projectAgentSession(session: SessionRecord) {
27
27
  };
28
28
  }
29
29
 
30
- export function projectAgentSessionTimelineEvent(
31
- event: SessionEventEnvelope,
32
- ) {
30
+ export function projectAgentSessionTimelineEvent(event: SessionEventEnvelope) {
33
31
  const data = event.data;
34
32
  return {
35
33
  sequence: event.sequence,
@@ -39,6 +37,9 @@ export function projectAgentSessionTimelineEvent(
39
37
  ...("runId" in data && data.runId ? { runId: data.runId } : {}),
40
38
  ...("turnId" in data ? { turnId: data.turnId } : {}),
41
39
  ...("stepId" in data ? { stepId: data.stepId } : {}),
40
+ ...("toolCallId" in data ? { toolCallId: data.toolCallId } : {}),
41
+ ...("toolKey" in data ? { toolKey: data.toolKey } : {}),
42
+ ...("outcome" in data ? { outcome: data.outcome } : {}),
42
43
  };
43
44
  }
44
45
 
@@ -121,17 +122,19 @@ export function projectableMemory(
121
122
  }
122
123
 
123
124
  function timelineSummary(type: SessionEventEnvelope["data"]["type"]) {
124
- return ({
125
- "session.created": "Session 已创建",
126
- "session.archived": "Session 已归档",
127
- "turn.started": "Turn 已开始",
128
- "turn.completed": "Turn 已结束",
129
- "run.linked": "Run 已关联",
130
- "step.started": "Step 已开始",
131
- "step.completed": "Step 已结束",
132
- "tool.execution-recorded": "Tool 执行事实已记录",
133
- "checkpoint.created": "Checkpoint 已创建",
134
- "fork.created": "Fork 已创建",
135
- "write-tool.reexecuted": "写 Tool 已经显式授权重新执行",
136
- } satisfies Record<SessionEventEnvelope["data"]["type"], string>)[type];
125
+ return (
126
+ {
127
+ "session.created": "Session 已创建",
128
+ "session.archived": "Session 已归档",
129
+ "turn.started": "Turn 已开始",
130
+ "turn.completed": "Turn 已结束",
131
+ "run.linked": "Run 已关联",
132
+ "step.started": "Step 已开始",
133
+ "step.completed": "Step 已结束",
134
+ "tool.execution-recorded": "Tool 执行事实已记录",
135
+ "checkpoint.created": "Checkpoint 已创建",
136
+ "fork.created": "Fork 已创建",
137
+ "write-tool.reexecuted": "写 Tool 已经显式授权重新执行",
138
+ } satisfies Record<SessionEventEnvelope["data"]["type"], string>
139
+ )[type];
137
140
  }
@@ -0,0 +1,131 @@
1
+ import { expect, test } from "bun:test";
2
+ import { SessionEventLogService } from "./session-log/service.js";
3
+ import { InMemorySessionEventRepository } from "./session-log/in-memory-repository.js";
4
+ import { projectSessionRecording } from "./session-recording-reader.js";
5
+
6
+ test("fork replay reuses AH1 recorded-only segments and original parent/child sequences", async () => {
7
+ const repository = new InMemorySessionEventRepository();
8
+ const service = new SessionEventLogService(repository);
9
+ const governance = {
10
+ actorId: "actor-1",
11
+ scopeDigest: `sha256:${"a".repeat(64)}` as const,
12
+ };
13
+ await service.create({
14
+ sessionId: "session_parent",
15
+ createdEventId: "session_event_parent",
16
+ governance,
17
+ });
18
+ await service.append({
19
+ sessionId: "session_parent",
20
+ expectedSequence: 1,
21
+ events: [
22
+ service.newEvent({
23
+ type: "turn.started",
24
+ turnId: "session_turn_parent",
25
+ cause: "user-input",
26
+ input: {
27
+ sourceType: "agent-run-input",
28
+ sourceId: "binding_1",
29
+ sourceVersion: "business.answer",
30
+ contentDigest: `sha256:${"b".repeat(64)}`,
31
+ },
32
+ }),
33
+ service.newEvent({
34
+ type: "run.linked",
35
+ turnId: "session_turn_parent",
36
+ runId: "run_parent",
37
+ }),
38
+ service.newEvent({
39
+ type: "turn.completed",
40
+ turnId: "session_turn_parent",
41
+ outcome: "completed",
42
+ }),
43
+ ],
44
+ });
45
+ await service.checkpoint({
46
+ sessionId: "session_parent",
47
+ expectedSequence: 4,
48
+ checkpointId: "session_checkpoint_parent",
49
+ eventId: "session_event_checkpoint",
50
+ });
51
+ await service.fork({
52
+ parentSessionId: "session_parent",
53
+ expectedParentSequence: 5,
54
+ checkpointId: "session_checkpoint_parent",
55
+ childSessionId: "session_child",
56
+ parentEventId: "session_event_fork",
57
+ childCreatedEventId: "session_event_child",
58
+ governance,
59
+ });
60
+ await service.append({
61
+ sessionId: "session_child",
62
+ expectedSequence: 1,
63
+ events: [
64
+ service.newEvent({
65
+ type: "turn.started",
66
+ turnId: "session_turn_child",
67
+ cause: "user-input",
68
+ input: {
69
+ sourceType: "agent-run-input",
70
+ sourceId: "binding_2",
71
+ sourceVersion: "business.answer",
72
+ contentDigest: `sha256:${"c".repeat(64)}`,
73
+ },
74
+ }),
75
+ service.newEvent({
76
+ type: "run.linked",
77
+ turnId: "session_turn_child",
78
+ runId: "run_child",
79
+ }),
80
+ service.newEvent({
81
+ type: "turn.completed",
82
+ turnId: "session_turn_child",
83
+ outcome: "completed",
84
+ }),
85
+ ],
86
+ });
87
+ const replay = await service.replay({ sessionId: "session_child" });
88
+ const projected = projectSessionRecording({
89
+ ...replay,
90
+ segments: await Promise.all(
91
+ replay.segments.map(async (segment) => ({
92
+ ...segment,
93
+ session: (await repository.get(segment.sessionId))!,
94
+ })),
95
+ ),
96
+ });
97
+ expect(replay.sideEffectPolicy).toBe("recorded-only");
98
+ expect(projected.segments.map(({ sessionId }) => sessionId)).toEqual([
99
+ "session_parent",
100
+ "session_child",
101
+ ]);
102
+ expect(
103
+ projected.segments.map(({ timeline }) =>
104
+ timeline.map(({ sequence }) => sequence),
105
+ ),
106
+ ).toEqual([
107
+ [1, 2, 3, 4],
108
+ [1, 2, 3, 4],
109
+ ]);
110
+ expect(
111
+ projected.runRefs.map(({ runId, sessionId, operationKey }) => ({
112
+ runId,
113
+ sessionId,
114
+ operationKey,
115
+ })),
116
+ ).toEqual([
117
+ {
118
+ runId: "run_parent",
119
+ sessionId: "session_parent",
120
+ operationKey: "business.answer",
121
+ },
122
+ {
123
+ runId: "run_child",
124
+ sessionId: "session_child",
125
+ operationKey: "business.answer",
126
+ },
127
+ ]);
128
+ expect(projected.truncated).toBe(false);
129
+ expect((await repository.get("session_parent"))?.headSequence).toBe(6);
130
+ expect((await repository.get("session_child"))?.headSequence).toBe(4);
131
+ });
@@ -0,0 +1,62 @@
1
+ import type { SessionRecord } from "./session-log/domain.js";
2
+ import type { ReadOnlySessionReplay } from "./session-log/replay-policy.js";
3
+ import {
4
+ projectAgentSession,
5
+ projectAgentSessionTimelineEvent,
6
+ } from "./session-projection-view.js";
7
+
8
+ /** Already-authorized replay segments preserve parent/child sequence domains. */
9
+ export function projectSessionRecording(
10
+ replay: Omit<ReadOnlySessionReplay, "segments"> & {
11
+ segments: readonly {
12
+ sessionId: string;
13
+ throughSequence: number;
14
+ events: ReadOnlySessionReplay["segments"][number]["events"];
15
+ session: SessionRecord;
16
+ }[];
17
+ },
18
+ ) {
19
+ const current = replay.segments.find(
20
+ ({ sessionId }) => sessionId === replay.sessionId,
21
+ );
22
+ if (!current) throw new Error("Current replay segment missing");
23
+ const segments = replay.segments.map((segment) => ({
24
+ ...segment,
25
+ events: segment.events.slice(0, 10_000),
26
+ }));
27
+ const runRefs = segments.flatMap(({ sessionId, events }) => {
28
+ const operations = new Map(
29
+ events.flatMap(({ data }) =>
30
+ data.type === "turn.started" &&
31
+ data.input.sourceType === "agent-run-input"
32
+ ? [[data.turnId, data.input.sourceVersion] as const]
33
+ : [],
34
+ ),
35
+ );
36
+ return events.flatMap(({ data }) =>
37
+ data.type === "run.linked"
38
+ ? [
39
+ {
40
+ runId: data.runId,
41
+ sessionId,
42
+ turnId: data.turnId,
43
+ operationKey: operations.get(data.turnId) ?? "",
44
+ },
45
+ ]
46
+ : [],
47
+ );
48
+ });
49
+ return {
50
+ session: projectAgentSession(current.session),
51
+ truncated: replay.segments.some(({ events }) => events.length > 10_000),
52
+ segments: segments.map(({ sessionId, throughSequence, events }) => ({
53
+ sessionId,
54
+ throughSequence,
55
+ timeline: events.map(projectAgentSessionTimelineEvent),
56
+ })),
57
+ timeline: segments
58
+ .find(({ sessionId }) => sessionId === replay.sessionId)!
59
+ .events.map(projectAgentSessionTimelineEvent),
60
+ runRefs,
61
+ };
62
+ }
@@ -0,0 +1,22 @@
1
+ import { randomUUIDv7 } from "bun";
2
+ import { auditActor, type RequestGuardContext } from "../guards.js";
3
+ import type { createServerRuntime } from "../runtime.js";
4
+
5
+ export function auditAgentAdminCatalogRead(
6
+ runtime: ReturnType<typeof createServerRuntime>,
7
+ context: RequestGuardContext,
8
+ kind: "tools" | "operations",
9
+ itemCount: number,
10
+ ): void {
11
+ runtime.auditSink.recordEvent({
12
+ id: randomUUIDv7(),
13
+ action:
14
+ kind === "tools" ? "system.agent-tools.read" : "system.agent-debug.read",
15
+ actor: auditActor(context),
16
+ outcome: "success",
17
+ target: { type: "agent-admin-catalog", id: kind },
18
+ requestId: context.requestId,
19
+ metadata: { itemCount },
20
+ occurredAt: new Date().toISOString(),
21
+ });
22
+ }
@@ -0,0 +1,21 @@
1
+ import type { AgentAdminAvailabilityReason } from "@southwind-ai/shared";
2
+ import type { GuardDecisionReason } from "../guards.js";
3
+
4
+ export function catalogGuardReason(
5
+ reason: GuardDecisionReason,
6
+ ): AgentAdminAvailabilityReason {
7
+ switch (reason) {
8
+ case "allowed":
9
+ case "anonymous":
10
+ return "allowed";
11
+ case "feature-disabled":
12
+ case "dependency-missing":
13
+ return "feature-unavailable";
14
+ case "license-restricted":
15
+ case "license-version-not-allowed":
16
+ case "entitlement-not-granted":
17
+ return "license-unavailable";
18
+ default:
19
+ return "permission-denied";
20
+ }
21
+ }