@piagent/platform 1.5.1 → 1.5.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 (125) hide show
  1. package/CHANGELOG.md +44 -1
  2. package/README.md +8 -2
  3. package/README.vi.md +23 -1
  4. package/benchmarks/capability-v1/DESIGN.md +30 -0
  5. package/benchmarks/capability-v1/coverage.json +23 -1
  6. package/benchmarks/capability-v1/grade.mjs +246 -0
  7. package/benchmarks/capability-v1/project/apps/web/src/control-view.js +3 -0
  8. package/benchmarks/capability-v1/project/apps/web/src/timeline-view.js +3 -0
  9. package/benchmarks/capability-v1/project/packages/session/src/admission.js +7 -0
  10. package/benchmarks/capability-v1/project/packages/session/src/runtime-route.js +3 -0
  11. package/benchmarks/capability-v1/project/packages/timeline/src/checkpoint.js +7 -0
  12. package/benchmarks/capability-v1/project/packages/timeline/src/project.js +7 -0
  13. package/benchmarks/capability-v1/project/test/smoke.test.js +19 -1
  14. package/benchmarks/capability-v1/prompts/causal-timeline-recovery.md +47 -0
  15. package/benchmarks/capability-v1/prompts/durable-session-control-plane.md +47 -0
  16. package/benchmarks/capability-v1/references/causal-timeline-recovery/apps/web/src/timeline-view.js +9 -0
  17. package/benchmarks/capability-v1/references/causal-timeline-recovery/packages/timeline/src/checkpoint.js +22 -0
  18. package/benchmarks/capability-v1/references/causal-timeline-recovery/packages/timeline/src/project.js +77 -0
  19. package/benchmarks/capability-v1/references/durable-session-control-plane/apps/web/src/control-view.js +4 -0
  20. package/benchmarks/capability-v1/references/durable-session-control-plane/packages/session/src/admission.js +52 -0
  21. package/benchmarks/capability-v1/references/durable-session-control-plane/packages/session/src/runtime-route.js +27 -0
  22. package/benchmarks/capability-v1/rubric.json +26 -0
  23. package/benchmarks/capability-v1/suite.json +32 -4
  24. package/benchmarks/capability-v1/variant.mjs +11 -0
  25. package/benchmarks/deep-logic-v1/grade.mjs +165 -0
  26. package/benchmarks/deep-logic-v1/project/README.md +3 -0
  27. package/benchmarks/deep-logic-v1/project/package.json +6 -0
  28. package/benchmarks/deep-logic-v1/project/src/config-transaction.js +3 -0
  29. package/benchmarks/deep-logic-v1/project/src/context-pack.js +3 -0
  30. package/benchmarks/deep-logic-v1/project/src/event-reconcile.js +3 -0
  31. package/benchmarks/deep-logic-v1/project/src/policy.js +3 -0
  32. package/benchmarks/deep-logic-v1/project/src/scheduler.js +3 -0
  33. package/benchmarks/deep-logic-v1/project/src/stream.js +3 -0
  34. package/benchmarks/deep-logic-v1/project/test/smoke.test.js +20 -0
  35. package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +13 -0
  36. package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +13 -0
  37. package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +14 -0
  38. package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +13 -0
  39. package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +14 -0
  40. package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +13 -0
  41. package/benchmarks/deep-logic-v1/suite.json +95 -0
  42. package/benchmarks/deep-logic-v1/variant.mjs +28 -0
  43. package/docs/command-reference-vietnamese.md +5 -2
  44. package/docs/distribution-standard.md +7 -7
  45. package/docs/languages.json +2 -2
  46. package/docs/operator-manual-vietnamese.md +7 -7
  47. package/docs/publishing-for-teams.md +7 -0
  48. package/docs/quality-benchmark.md +49 -10
  49. package/docs/release-install-policy.md +6 -6
  50. package/docs/security-threat-model.md +1 -1
  51. package/docs/team-onboarding.md +2 -2
  52. package/docs-site/architecture.html +1 -1
  53. package/docs-site/benchmark.html +1 -1
  54. package/docs-site/commands.html +1 -1
  55. package/docs-site/context-engine.html +1 -1
  56. package/docs-site/en/architecture.html +1 -1
  57. package/docs-site/en/benchmark.html +1 -1
  58. package/docs-site/en/commands.html +1 -1
  59. package/docs-site/en/context-engine.html +1 -1
  60. package/docs-site/en/index.html +1 -1
  61. package/docs-site/en/mcp-approval.html +1 -1
  62. package/docs-site/en/mcp-auth.html +1 -1
  63. package/docs-site/en/mcp-servers.html +1 -1
  64. package/docs-site/en/mcp.html +1 -1
  65. package/docs-site/en/permissions.html +1 -1
  66. package/docs-site/en/profiles.html +1 -1
  67. package/docs-site/en/quickstart.html +1 -1
  68. package/docs-site/en/runtime.html +1 -1
  69. package/docs-site/en/scope.html +1 -1
  70. package/docs-site/en/security.html +1 -1
  71. package/docs-site/en/team.html +1 -1
  72. package/docs-site/en/whats-new.html +1 -1
  73. package/docs-site/en/workflows.html +1 -1
  74. package/docs-site/index.html +1 -1
  75. package/docs-site/mcp-approval.html +1 -1
  76. package/docs-site/mcp-auth.html +1 -1
  77. package/docs-site/mcp-servers.html +1 -1
  78. package/docs-site/mcp.html +1 -1
  79. package/docs-site/permissions.html +1 -1
  80. package/docs-site/profiles.html +1 -1
  81. package/docs-site/quickstart.html +1 -1
  82. package/docs-site/runtime.html +1 -1
  83. package/docs-site/scope.html +1 -1
  84. package/docs-site/security.html +1 -1
  85. package/docs-site/team.html +1 -1
  86. package/docs-site/whats-new.html +1 -1
  87. package/docs-site/workflows.html +1 -1
  88. package/evals/fixtures/benchmark-assurance-evidence.invalid.json +2 -2
  89. package/evals/fixtures/benchmark-assurance-evidence.valid.json +2 -2
  90. package/evals/fixtures/piagent-webui/runtime-command-v1.invalid.json +12 -0
  91. package/evals/fixtures/piagent-webui/runtime-command-v1.valid.json +11 -0
  92. package/evals/fs4-readiness-matrix.v1.json +2 -2
  93. package/evals/fs5-causal-arm.v1.json +1 -1
  94. package/evals/fs5-pilot-protocol.v1.json +1 -1
  95. package/evals/fs5-pilot-protocol.v4.json +1 -1
  96. package/evals/fs5-pilot-protocol.v5.json +1 -1
  97. package/evals/private-holdout-v1/public-exposure.v1.json +3 -3
  98. package/evals/real-task-taxonomy.v1.json +5 -1
  99. package/package.json +3 -1
  100. package/packages/piagent-core/benchmark/benchmark-bootstrap.js +3 -2
  101. package/packages/piagent-core/benchmark/benchmark-cli.js +5 -1
  102. package/packages/piagent-core/benchmark/benchmark-matrix.js +1 -1
  103. package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +1 -0
  104. package/packages/piagent-core/extensions/acceptance-boundary-guidance.js +64 -0
  105. package/packages/piagent-core/extensions/acceptance-contract-semantics.js +3 -2
  106. package/packages/piagent-core/package.json +1 -1
  107. package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -4
  108. package/packages/piagent-core/runtime/registration/task-start-tool.ts +2 -2
  109. package/packages/piagent-core/runtime/workflows/webui-runtime-command.ts +89 -0
  110. package/packages/piagent-core/runtime/workflows/webui-workflow.ts +51 -0
  111. package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-Uouhx1QO.js → MarkdownMessage-BnvXTpou.js} +1 -1
  112. package/packages/piagent-webui/dist/client/assets/index-DZg979Ht.js +174 -0
  113. package/packages/piagent-webui/dist/client/index.html +1 -1
  114. package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
  115. package/packages/piagent-webui/gateway/runtime-command-controller.ts +126 -0
  116. package/packages/piagent-webui/gateway/runtime-session-controls.ts +29 -0
  117. package/packages/piagent-webui/gateway/session-command-controller.ts +23 -7
  118. package/packages/piagent-webui/gateway/session-inspection-registry.ts +22 -1
  119. package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +11 -9
  120. package/packages/piagent-webui/package.json +2 -1
  121. package/packages/piagent-webui/server/loopback-server.ts +17 -0
  122. package/schemas/piagent-webui/catalog-v1.json +9 -0
  123. package/schemas/piagent-webui/runtime-command-v1.schema.json +70 -0
  124. package/schemas/piagent-webui/session-command-v1.schema.json +8 -0
  125. package/packages/piagent-webui/dist/client/assets/index-MBZq8XLV.js +0 -174
@@ -7,7 +7,7 @@
7
7
  <meta name="csp-nonce" content="__PIAGENT_CSP_NONCE__" />
8
8
  <meta name="piagent-webui-mode" content="__PIAGENT_WEBUI_MODE__" />
9
9
  <title>Piagent WebUI</title>
10
- <script type="module" crossorigin src="/assets/index-MBZq8XLV.js"></script>
10
+ <script type="module" crossorigin src="/assets/index-DZg979Ht.js"></script>
11
11
  <link rel="stylesheet" crossorigin href="/assets/index-BV-_03yl.css">
12
12
  </head>
13
13
  <body>
@@ -23,6 +23,7 @@ import { SessionLeaseStore } from "./session-lease-store.ts";
23
23
  import { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
24
24
  import { SessionCommandStore } from "./session-command-store.ts";
25
25
  import { SessionCommandController } from "./session-command-controller.ts";
26
+ import { RuntimeCommandController } from "./runtime-command-controller.ts";
26
27
  import { GatewayEventStore } from "./gateway-events.ts";
27
28
  import { SessionInspectionRegistry } from "./session-inspection-registry.ts";
28
29
  import { SessionAttachmentRegistry } from "./session-attachment-registry.ts";
@@ -175,6 +176,7 @@ export async function startPiagentGateway(options: {
175
176
  if (!attachments) throw new Error("session-attachment-unavailable");
176
177
  return attachments.reserveForPrompt(sessionRef, refs, messageRequestId, text);
177
178
  } });
179
+ const runtimeCommands = new RuntimeCommandController({ catalog: readCatalog, runtimes, events });
178
180
  const protocol = new GatewayProtocolService({ capabilities: () => capabilities(gatewayInstanceRef, true), catalog: readCatalog,
179
181
  events, command: commands });
180
182
  const inspections = new SessionInspectionRegistry({ gatewayInstanceRef, host, key, packageRoot: options.packageRoot, agentDir: state.agentDir,
@@ -194,6 +196,7 @@ export async function startPiagentGateway(options: {
194
196
  readSessionModel: (sessionRef) => inspections.provider(sessionRef),
195
197
  readSessionConnections: (sessionRef) => inspections.connections(sessionRef),
196
198
  executeSessionConnection: (command) => inspections.executeConnectionCommand(command),
199
+ executeRuntimeCommand: (command) => runtimeCommands.execute(command),
197
200
  executeSessionAttachment: (sessionRef, command) => attachments.execute(sessionRef, command),
198
201
  executeApproval: (approvalRef, decision) => runtimes!.decideApproval(approvalRef, decision),
199
202
  readMcpAuthJob: (jobRef) => mcpAuth!.read(jobRef),
@@ -0,0 +1,126 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import {
4
+ WEBUI_RUNTIME_ACTIONS,
5
+ buildWebUiRuntimeCommand,
6
+ isWebUiRuntimeActionId
7
+ } from "../../piagent-core/runtime/workflows/webui-runtime-command.ts";
8
+ import type { Catalog } from "../contracts/generated/session-catalog-v1.ts";
9
+ import type { Action, Command, Receipt } from "../contracts/generated/runtime-command-v1.ts";
10
+ import { GatewayEventStore } from "./gateway-events.ts";
11
+ import { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
12
+
13
+ const REF = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
14
+ const REVISION = /^[A-Za-z0-9][A-Za-z0-9._:~-]{0,159}$/;
15
+ const MAX_REPLAYS = 200;
16
+
17
+ type Replay = { digest: string; receipt: Receipt };
18
+
19
+ function record(value: unknown): value is Record<string, unknown> {
20
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
21
+ }
22
+
23
+ function parseCommand(value: unknown): Command | null {
24
+ if (!record(value)) return null;
25
+ const expected = ["schemaVersion", "version", "messageType", "requestId", "sessionRef", "expectedSessionRevision", "action", "argument", "confirmed"];
26
+ const keys = Object.keys(value);
27
+ if (keys.length !== expected.length || !keys.every((key) => expected.includes(key))) return null;
28
+ if (value.schemaVersion !== 1 || value.version !== "piagent-runtime-command-v1" || value.messageType !== "command"
29
+ || typeof value.requestId !== "string" || !REF.test(value.requestId)
30
+ || typeof value.sessionRef !== "string" || !REF.test(value.sessionRef)
31
+ || typeof value.expectedSessionRevision !== "string" || !REVISION.test(value.expectedSessionRevision)
32
+ || !isWebUiRuntimeActionId(value.action)
33
+ || !(value.argument === null || typeof value.argument === "string" && value.argument.length <= 2_048 && !value.argument.includes("\0"))
34
+ || typeof value.confirmed !== "boolean") return null;
35
+ return value as unknown as Command;
36
+ }
37
+
38
+ function commandDigest(command: Command): string {
39
+ return createHash("sha256").update(JSON.stringify({ sessionRef: command.sessionRef,
40
+ expectedSessionRevision: command.expectedSessionRevision, action: command.action,
41
+ argument: command.argument, confirmed: command.confirmed })).digest("hex");
42
+ }
43
+
44
+ export class RuntimeCommandController {
45
+ readonly #catalog: () => Promise<Catalog>;
46
+ readonly #runtimes: SessionRuntimeSupervisor;
47
+ readonly #events: GatewayEventStore;
48
+ readonly #replays = new Map<string, Replay>();
49
+ readonly #chains = new Map<string, Promise<unknown>>();
50
+
51
+ constructor(options: { catalog(): Promise<Catalog>; runtimes: SessionRuntimeSupervisor; events: GatewayEventStore }) {
52
+ this.#catalog = options.catalog;
53
+ this.#runtimes = options.runtimes;
54
+ this.#events = options.events;
55
+ }
56
+
57
+ execute(input: unknown): Promise<Receipt> {
58
+ const command = parseCommand(input);
59
+ if (!command) return Promise.reject(new Error("runtime-command-invalid"));
60
+ const prior = this.#chains.get(command.sessionRef) ?? Promise.resolve();
61
+ const run = prior.then(() => this.#execute(command), () => this.#execute(command));
62
+ const tail = run.catch(() => undefined);
63
+ this.#chains.set(command.sessionRef, tail);
64
+ return run.finally(() => { if (this.#chains.get(command.sessionRef) === tail) this.#chains.delete(command.sessionRef); });
65
+ }
66
+
67
+ async #execute(command: Command): Promise<Receipt> {
68
+ const digest = commandDigest(command), replay = this.#replays.get(command.requestId);
69
+ if (replay) {
70
+ if (replay.digest !== digest) return this.#receipt(command, "rejected", "invalid-command", "idempotency-payload-mismatch");
71
+ return replay.receipt;
72
+ }
73
+ const catalog = await this.#readyCatalog();
74
+ const row = catalog.sessions.find((session) => session.sessionRef === command.sessionRef);
75
+ if (!row || row.archived) return this.#settle(command, digest,
76
+ this.#receipt(command, "rejected", "unavailable", row ? "session-archived" : "session-not-found"));
77
+ if (row.sessionRevision !== command.expectedSessionRevision) return this.#settle(command, digest,
78
+ this.#receipt(command, "rejected", "stale-revision", "session-revision-stale", row.sessionRevision));
79
+
80
+ let built: ReturnType<typeof buildWebUiRuntimeCommand>;
81
+ try { built = buildWebUiRuntimeCommand({ action: command.action, argument: command.argument, confirmed: command.confirmed }); }
82
+ catch (error) {
83
+ const reason = error instanceof Error ? error.message : "runtime-command-invalid";
84
+ return this.#settle(command, digest, this.#receipt(command, "rejected",
85
+ reason === "runtime-command-confirmation-required" ? "confirmation-required" : "invalid-command", reason, row.sessionRevision));
86
+ }
87
+
88
+ try {
89
+ const result = await this.#runtimes.runRuntimeCommand(command.sessionRef, built.command);
90
+ const after = await this.#readyCatalog();
91
+ const afterRow = after.sessions.find((session) => session.sessionRef === command.sessionRef);
92
+ const zeroTurnViolation = built.spec.effect === "read-only" && result.modelCallObserved;
93
+ const receipt = this.#receipt(command, zeroTurnViolation ? "uncertain" : "settled",
94
+ zeroTurnViolation ? "effect-unknown" : "completed",
95
+ zeroTurnViolation ? "read-only-command-started-model-call" : null,
96
+ afterRow?.sessionRevision ?? null, result.modelCallObserved, result.outputs as Receipt["outputs"]);
97
+ this.#events.publish("session.changed", { catalogRevision: after.catalogRevision, session: afterRow ?? row });
98
+ return this.#settle(command, digest, receipt);
99
+ } catch (error) {
100
+ const reason = error instanceof Error ? error.message : "runtime-command-unavailable";
101
+ const resultCode = reason.includes("busy") ? "runtime-busy" : "unavailable";
102
+ return this.#settle(command, digest,
103
+ this.#receipt(command, "rejected", resultCode, reason, row.sessionRevision));
104
+ }
105
+ }
106
+
107
+ async #readyCatalog(): Promise<Catalog> {
108
+ const catalog = await this.#catalog();
109
+ if (catalog.state !== "ready" || !catalog.catalogRevision) throw new Error(catalog.reasonCode ?? "catalog-unavailable");
110
+ return catalog;
111
+ }
112
+
113
+ #receipt(command: Command, state: Receipt["state"], resultCode: Receipt["resultCode"], reasonCode: string | null,
114
+ sessionRevisionAfter: string | null = null, modelCallObserved = false, outputs: Receipt["outputs"] = []): Receipt {
115
+ const spec = WEBUI_RUNTIME_ACTIONS.find((item) => item.id === command.action)!;
116
+ return { schemaVersion: 1, version: "piagent-runtime-receipt-v1", messageType: "receipt",
117
+ requestId: command.requestId, sessionRef: command.sessionRef, action: command.action as Action,
118
+ state, resultCode, effect: spec.effect, modelCallObserved, outputs, sessionRevisionAfter, reasonCode };
119
+ }
120
+
121
+ #settle(command: Command, digest: string, receipt: Receipt): Receipt {
122
+ this.#replays.set(command.requestId, { digest, receipt });
123
+ while (this.#replays.size > MAX_REPLAYS) this.#replays.delete(this.#replays.keys().next().value as string);
124
+ return receipt;
125
+ }
126
+ }
@@ -0,0 +1,29 @@
1
+ import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
2
+
3
+ export async function executePermissionCommand(session: any,
4
+ permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<void> {
5
+ const before = Array.isArray(session.messages) ? session.messages.length : 0;
6
+ await session.prompt(`/permission ${permissionMode}`);
7
+ const messages = Array.isArray(session.messages) ? session.messages.slice(before) : [];
8
+ const observed = messages.reverse().find((message: any) => message?.role === "custom"
9
+ && message.customType === "piagent-permission-profile");
10
+ const profile = observed?.details?.permissionProfile;
11
+ if (!profile || profile.mode !== permissionMode || profile.warning) throw new Error("session-permission-unavailable");
12
+ }
13
+
14
+ export async function executeRuntimeCommand(session: any, command: string): Promise<{
15
+ outputs: Array<{ customType: string; content: string; truncated: boolean; redacted: boolean }>;
16
+ modelCallObserved: boolean;
17
+ }> {
18
+ const before = Array.isArray(session.messages) ? session.messages.length : 0;
19
+ await session.prompt(command);
20
+ const added = Array.isArray(session.messages) ? session.messages.slice(before) : [];
21
+ const outputs = added.filter((message: any) => message?.role === "custom").slice(-8).map((message: any) => {
22
+ const source = typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? "");
23
+ const bounded = source.slice(0, 12_000), redacted = redactSensitiveText(bounded);
24
+ const rawType = String(message.customType ?? "runtime-output");
25
+ return { customType: /^[A-Za-z0-9._-]{1,120}$/.test(rawType) ? rawType : "runtime-output", content: redacted.text,
26
+ truncated: source.length > bounded.length, redacted: redacted.redacted };
27
+ });
28
+ return { outputs, modelCallObserved: added.some((message: any) => message?.role === "assistant") };
29
+ }
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
2
2
 
3
3
  import type { Catalog, SessionRow } from "../contracts/generated/session-catalog-v1.ts";
4
4
  import type { Receipt } from "../contracts/generated/session-command-v1.ts";
5
+ import { buildWebUiWorkflowCommand, isWebUiWorkflowId, type WebUiWorkflowId } from "../../piagent-core/runtime/workflows/webui-workflow.ts";
5
6
  import { GatewayEventStore } from "./gateway-events.ts";
6
7
  import { SessionCommandStore, type SessionAction, type SessionCommandIdentity } from "./session-command-store.ts";
7
8
  import { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
@@ -27,6 +28,10 @@ function record(value: unknown): value is Record<string, unknown> { return Boole
27
28
  function exactKeys(value: Record<string, unknown>, keys: string[]): boolean {
28
29
  const found = Object.keys(value); return found.length === keys.length && found.every((key) => keys.includes(key));
29
30
  }
31
+ function requiredOptionalKeys(value: Record<string, unknown>, required: string[], optional: string[]): boolean {
32
+ const found = Object.keys(value), allowed = new Set([...required, ...optional]);
33
+ return required.every((key) => found.includes(key)) && found.every((key) => allowed.has(key));
34
+ }
30
35
  function exactTimestamp(value: unknown): value is string {
31
36
  return typeof value === "string" && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(value)
32
37
  && Number.isFinite(Date.parse(value)) && new Date(value).toISOString() === value;
@@ -35,13 +40,15 @@ function nullableRef(value: unknown): value is string | null { return value ===
35
40
 
36
41
  function validPayload(action: SessionAction, payload: Record<string, unknown>): boolean {
37
42
  if (["session.archive", "session.unarchive", "session.acquire", "session.release"].includes(action)) return exactKeys(payload, []);
38
- if (action === "session.send") return exactKeys(payload, ["delivery", "message", "messageRequestId", "expectedOperationRef", "attachmentRefs"])
43
+ if (action === "session.send") return requiredOptionalKeys(payload,
44
+ ["delivery", "message", "messageRequestId", "expectedOperationRef", "attachmentRefs"], ["workflow"])
39
45
  && Array.isArray(payload.attachmentRefs) && payload.attachmentRefs.length <= 4
40
46
  && new Set(payload.attachmentRefs as unknown[]).size === payload.attachmentRefs.length
41
47
  && (payload.attachmentRefs as unknown[]).every((item) => typeof item === "string" && REF.test(item))
42
48
  && ["new-operation", "follow-up", "steer"].includes(String(payload.delivery)) && typeof payload.message === "string"
43
49
  && payload.message.length >= 1 && payload.message.length <= 32_768 && !payload.message.includes("\0") && REF.test(String(payload.messageRequestId))
44
- && nullableRef(payload.expectedOperationRef) && (payload.delivery === "new-operation" ? payload.expectedOperationRef === null : payload.expectedOperationRef !== null);
50
+ && nullableRef(payload.expectedOperationRef) && (payload.delivery === "new-operation" ? payload.expectedOperationRef === null : payload.expectedOperationRef !== null)
51
+ && (payload.workflow === undefined || payload.delivery === "new-operation" && isWebUiWorkflowId(payload.workflow));
45
52
  if (action === "session.abort") return exactKeys(payload, ["operationRef", "clearQueued"])
46
53
  && REF.test(String(payload.operationRef)) && typeof payload.clearQueued === "boolean";
47
54
  if (action === "session.set-model") return exactKeys(payload, ["modelRef"]) && REF.test(String(payload.modelRef));
@@ -56,11 +63,13 @@ function validPayload(action: SessionAction, payload: Record<string, unknown>):
56
63
  && (payload.title === null || typeof payload.title === "string" && payload.title.length >= 1 && payload.title.length <= 500
57
64
  && !/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(payload.title));
58
65
  const createKeys = ["projectRef", "placeRef", "modelRef", "thinkingLevel", "message", "messageRequestId"];
59
- return (exactKeys(payload, createKeys) || exactKeys(payload, [...createKeys, "deferInitialMessage"]))
66
+ return requiredOptionalKeys(payload, createKeys, ["deferInitialMessage", "permissionMode", "workflow"])
60
67
  && REF.test(String(payload.projectRef)) && REF.test(String(payload.placeRef)) && nullableRef(payload.modelRef)
61
68
  && ["off", "minimal", "low", "medium", "high", "xhigh", "max"].includes(String(payload.thinkingLevel))
62
69
  && typeof payload.message === "string" && payload.message.length >= 1 && payload.message.length <= 32_768 && !payload.message.includes("\0")
63
- && REF.test(String(payload.messageRequestId)) && (payload.deferInitialMessage === undefined || typeof payload.deferInitialMessage === "boolean");
70
+ && REF.test(String(payload.messageRequestId)) && (payload.deferInitialMessage === undefined || typeof payload.deferInitialMessage === "boolean")
71
+ && (payload.permissionMode === undefined || ["read-only", "workspace-write", "trusted-full-access"].includes(String(payload.permissionMode)))
72
+ && (payload.workflow === undefined || isWebUiWorkflowId(payload.workflow));
64
73
  }
65
74
 
66
75
  function parseCommand(value: unknown): SessionCommand | null {
@@ -147,11 +156,17 @@ export class SessionCommandController {
147
156
  if (command.action === "session.create") {
148
157
  targetSessionRef = await this.#runtimes.create(String(command.payload.projectRef), String(command.payload.placeRef),
149
158
  command.payload.modelRef === null ? null : String(command.payload.modelRef), String(command.payload.thinkingLevel));
159
+ if (command.payload.permissionMode !== undefined) {
160
+ await this.#runtimes.setPermission(targetSessionRef, String(command.payload.permissionMode) as
161
+ "read-only" | "workspace-write" | "trusted-full-access");
162
+ }
150
163
  const created = await this.#readyCatalog(), createdRow = created.sessions.find((item) => item.sessionRef === targetSessionRef);
151
164
  if (!createdRow) throw new Error("session-catalog-refresh-failed");
152
165
  if (command.payload.deferInitialMessage === true) resultCode = "created";
153
166
  else {
154
- const sent = await this.#runtimes.send(targetSessionRef, { delivery: "new-operation", message: String(command.payload.message),
167
+ const message = buildWebUiWorkflowCommand((command.payload.workflow as WebUiWorkflowId | undefined) ?? null,
168
+ String(command.payload.message));
169
+ const sent = await this.#runtimes.send(targetSessionRef, { delivery: "new-operation", message,
155
170
  expectedOperationRef: null }, createdRow.sessionRevision);
156
171
  resultCode = sent.resultCode === "started" ? "started" : sent.resultCode; operationRef = sent.operationRef;
157
172
  }
@@ -162,7 +177,7 @@ export class SessionCommandController {
162
177
  const acquired = await this.#readyCatalog(), acquiredRow = acquired.sessions.find((item) => item.sessionRef === command.sessionRef);
163
178
  if (!acquiredRow) throw new Error("session-catalog-refresh-failed");
164
179
  const payload = command.payload as { delivery: "new-operation" | "follow-up" | "steer"; message: string;
165
- expectedOperationRef: string | null; messageRequestId: string; attachmentRefs: string[] };
180
+ expectedOperationRef: string | null; messageRequestId: string; attachmentRefs: string[]; workflow?: WebUiWorkflowId };
166
181
  // Staged refs are claimed once, here, before the runtime is asked to
167
182
  // dispatch. Documents were turned into text at staging so they join the
168
183
  // message; images travel on the host's own channel.
@@ -172,7 +187,8 @@ export class SessionCommandController {
172
187
  attachmentReservation = prepared;
173
188
  if (!this.#prepareAttachments && payload.attachmentRefs.length > 0) throw new Error("session-attachment-unavailable");
174
189
  sendAttempted = true;
175
- const sent = await this.#runtimes.send(command.sessionRef!, { ...payload, message: prepared.text, images: prepared.images },
190
+ const message = buildWebUiWorkflowCommand(payload.workflow ?? null, prepared.text);
191
+ const sent = await this.#runtimes.send(command.sessionRef!, { ...payload, message, images: prepared.images },
176
192
  acquiredRow.sessionRevision);
177
193
  prepared.commit();
178
194
  resultCode = sent.resultCode; operationRef = sent.operationRef;
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import fs from "node:fs";
2
3
  import path from "node:path";
3
4
 
4
5
  import { readContextTelemetry } from "../../piagent-core/extensions/context-engine.js";
@@ -9,6 +10,8 @@ import { webUiModelRef } from "../../piagent-core/runtime/inspection/webui-snaps
9
10
  import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
10
11
  import { piApprovalBroker } from "../../piagent-core/runtime/inspection/approval-broker.ts";
11
12
  import { attachmentCapability } from "../../piagent-core/runtime/input/attachment-store.ts";
13
+ import { WEBUI_WORKFLOW_OPTIONS } from "../../piagent-core/runtime/workflows/webui-workflow.ts";
14
+ import { WEBUI_RUNTIME_ACTIONS } from "../../piagent-core/runtime/workflows/webui-runtime-command.ts";
12
15
  import { CoreInspectionProvider } from "../server/core-inspection-provider.ts";
13
16
  import { ReadModelNotFound, type WebUiReadModelProvider } from "../server/read-model-provider.ts";
14
17
  import { isUserConversationSession, projectRefForCwd, sessionRefForPath, type PiSessionInfo } from "./session-catalog.ts";
@@ -189,6 +192,16 @@ export class SessionInspectionRegistry {
189
192
  if (!projects.has(project.projectRef)) projects.set(project.projectRef, project);
190
193
  }
191
194
  const availableModels = (this.#models?.getAvailableSnapshot() ?? []).slice(0, 300);
195
+ const profiles = safeRead(() => fs.readdirSync(path.join(this.#packageRoot, "adapters")).flatMap((entry) => {
196
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/.test(entry)) return [];
197
+ const file = path.join(this.#packageRoot, "adapters", entry, "profile.json");
198
+ const stat = fs.lstatSync(file);
199
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1_048_576) return [];
200
+ const value = JSON.parse(fs.readFileSync(file, "utf8")) as Record<string, unknown>;
201
+ return [{ id: entry, displayName: safeName(value.displayName ?? entry),
202
+ permissionMode: ["read-only", "workspace-write", "trusted-full-access"].includes(String(value.permissionProfile))
203
+ ? String(value.permissionProfile) : null }];
204
+ }).slice(0, 100), []);
192
205
  const models = availableModels.flatMap((value) => {
193
206
  const provider = safeModelId(value.provider);
194
207
  const modelId = safeModelId(value.id);
@@ -201,8 +214,16 @@ export class SessionInspectionRegistry {
201
214
  return [{ modelRef: webUiModelRef(provider, modelId), provider, modelId, displayName: safeName(value.name ?? modelId),
202
215
  reasoning: value.reasoning, imageInput: inputs ? inputs.includes("image") : null, thinkingLevels }];
203
216
  });
217
+ // Keep the WebUI new-session baseline deterministic and visible. If the
218
+ // preferred model is not authenticated, the host's own default remains the
219
+ // safe fallback instead of silently substituting another catalog entry.
220
+ const defaultModel = models.find((value) => value.provider === "openai-codex" && value.modelId === "gpt-5.6-sol");
204
221
  return { schemaVersion: 1, version: "piagent-session-creation-options-v1", generatedAt: new Date().toISOString(),
205
- projects: [...projects.values()].slice(0, 200), models, webSearch: inspectWebSearchCapability({ agentDir: this.#agentDir, models: availableModels }),
222
+ projects: [...projects.values()].slice(0, 200), models,
223
+ defaultModelRef: defaultModel?.modelRef ?? null, defaultThinkingLevel: defaultModel ? "high" : null,
224
+ profiles, workflows: WEBUI_WORKFLOW_OPTIONS,
225
+ runtimeActions: WEBUI_RUNTIME_ACTIONS,
226
+ webSearch: inspectWebSearchCapability({ agentDir: this.#agentDir, models: availableModels }),
206
227
  projectImport: nativeProjectPickerAvailable() ? { status: "available", reasonCode: null }
207
228
  : { status: "unavailable", reasonCode: "native-project-picker-unavailable" },
208
229
  reasonCode: projects.size ? null : "no-known-project" };
@@ -13,6 +13,7 @@ import { GatewayEventStore } from "./gateway-events.ts";
13
13
  import { GatewaySessionStream } from "./gateway-session-stream.ts";
14
14
  import { GATEWAY_RUNTIME_UI_MARKER } from "../ownership/gateway-runtime-context.ts";
15
15
  import { preferAuthoritativePiagentGuard } from "./extension-authority.ts";
16
+ import { executePermissionCommand, executeRuntimeCommand } from "./runtime-session-controls.ts";
16
17
 
17
18
  const MAX_WARM_RUNTIMES = 10;
18
19
 
@@ -22,6 +23,7 @@ type ActiveRuntime = { runtime: RuntimeHandle; lease: SessionLeaseSnapshot; info
22
23
  unsubscribe: (() => void) | null; completion: Promise<void> | null; settling: boolean; approvalWaiting: boolean;
23
24
  unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null; sessionManager: any | null };
24
25
  type Projection = { sessionRevision: string; liveState: "offline" | "idle" | "running" | "paused" | "waiting-approval" | "uncertain" };
26
+ type RuntimeCommandResult = Awaited<ReturnType<typeof executeRuntimeCommand>>;
25
27
 
26
28
  function rpcUiContext(): object {
27
29
  const plain = (text: unknown): string => String(text ?? "");
@@ -389,23 +391,23 @@ export class SessionRuntimeSupervisor {
389
391
  return "thinking-changed";
390
392
  }
391
393
 
392
- async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"):
393
- Promise<"permission-changed"> {
394
+ async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<"permission-changed"> {
394
395
  await this.acquire(sessionRef);
395
396
  const active = this.#active.get(sessionRef), session = active?.runtime.session;
396
397
  if (!active || !session) throw new Error("session-runtime-unavailable");
397
398
  if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
398
- const before = Array.isArray(session.messages) ? session.messages.length : 0;
399
- await session.prompt(`/permission ${permissionMode}`);
400
- const messages = Array.isArray(session.messages) ? session.messages.slice(before) : [];
401
- const observed = messages.reverse().find((message: any) => message?.role === "custom"
402
- && message.customType === "piagent-permission-profile");
403
- const profile = observed?.details?.permissionProfile;
404
- if (!profile || profile.mode !== permissionMode || profile.warning) throw new Error("session-permission-unavailable");
399
+ await executePermissionCommand(session, permissionMode);
405
400
  this.#touchCreated(sessionRef);
406
401
  return "permission-changed";
407
402
  }
408
403
 
404
+ async runRuntimeCommand(sessionRef: string, command: string): Promise<RuntimeCommandResult> {
405
+ await this.acquire(sessionRef);
406
+ const active = this.#active.get(sessionRef), session = active?.runtime.session;
407
+ if (!active || !session) throw new Error("session-runtime-unavailable");
408
+ if (active.operationRef || active.settling || !session.isIdle) throw new Error("session-runtime-busy");
409
+ const result = await executeRuntimeCommand(session, command); this.#touchCreated(sessionRef); return result;
410
+ }
409
411
  approvalProjection(sessionRef: string): { revision: string | null; summary: Record<string, unknown> } {
410
412
  const active = this.#active.get(sessionRef);
411
413
  if (!active) return { revision: null, summary: { state: "unknown", pending: [], recent: [],
@@ -8,7 +8,8 @@
8
8
  "check:contracts": "node scripts/generate-contracts.mjs --check",
9
9
  "typecheck": "tsc -p tsconfig.json --noEmit",
10
10
  "build": "npm run check:contracts && vite build",
11
- "benchmark": "node benchmark/benchmark.mjs"
11
+ "benchmark": "node benchmark/benchmark.mjs",
12
+ "benchmark:parity": "node benchmark/parity-benchmark.mjs"
12
13
  },
13
14
  "dependencies": {
14
15
  "@emotion/cache": "^11.14.0",
@@ -61,6 +61,7 @@ export async function startLoopbackServer(options: {
61
61
  executeSessionAttachment?: (sessionRef: string, command: unknown) => unknown | Promise<unknown>;
62
62
  readSessionConnections?: (sessionRef: string) => unknown | Promise<unknown>;
63
63
  executeSessionConnection?: (command: unknown) => unknown | Promise<unknown>;
64
+ executeRuntimeCommand?: (command: unknown) => unknown | Promise<unknown>;
64
65
  readMcpAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
65
66
  cancelMcpAuthJob?: (jobRef: string) => unknown | Promise<unknown>;
66
67
  readProviderAuthCatalog?: () => unknown | Promise<unknown>;
@@ -205,6 +206,22 @@ export async function startLoopbackServer(options: {
205
206
  }
206
207
  }
207
208
 
209
+ if (request.method === "POST" && url.pathname === "/api/v1/runtime-commands" && options.executeRuntimeCommand) {
210
+ if (requestOrigin !== origin) return errorResponse(response, 403, "origin-required");
211
+ const mutationSession = auth.authorizeMutation(request);
212
+ if (!mutationSession) return errorResponse(response, 403, "mutation-authority-rejected");
213
+ if (!consumeControl(mutationSession.id, now)) return errorResponse(response, 429, "control-rate-limit");
214
+ if (!String(request.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) return errorResponse(response, 415, "content-type");
215
+ let command: unknown;
216
+ try { command = JSON.parse((await requestBody(request, MAX_CONTROL_BODY_BYTES)).toString("utf8")); }
217
+ catch (error) { return errorResponse(response, (error as Error).message === "body-limit" ? 413 : 400, "invalid-runtime-command"); }
218
+ try { return jsonResponse(response, 200, await options.executeRuntimeCommand(command)); }
219
+ catch (error) {
220
+ const code = error instanceof Error ? error.message : "runtime-command-unavailable";
221
+ return errorResponse(response, code.includes("invalid") ? 400 : 503, code);
222
+ }
223
+ }
224
+
208
225
  if (request.method === "POST" && url.pathname.startsWith("/api/v1/mcp-auth/jobs/") && url.pathname.endsWith("/cancel") && options.cancelMcpAuthJob) {
209
226
  if (requestOrigin !== origin) return errorResponse(response, 403, "mutation-authority-rejected");
210
227
  const mutationSession = auth.authorizeMutation(request);
@@ -227,6 +227,15 @@
227
227
  "supported": true,
228
228
  "current": true
229
229
  },
230
+ {
231
+ "name": "runtime-command-v1",
232
+ "file": "runtime-command-v1.schema.json",
233
+ "id": "https://piagent.io.vn/schemas/piagent-webui/runtime-command-v1.schema.json",
234
+ "documentVersion": 1,
235
+ "role": "command",
236
+ "supported": true,
237
+ "current": true
238
+ },
230
239
  {
231
240
  "name": "gateway-capabilities-v1",
232
241
  "file": "gateway-capabilities-v1.schema.json",
@@ -0,0 +1,70 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://piagent.io.vn/schemas/piagent-webui/runtime-command-v1.schema.json",
4
+ "title": "Piagent WebUI typed runtime command and receipt v1",
5
+ "oneOf": [
6
+ { "$ref": "#/$defs/command" },
7
+ { "$ref": "#/$defs/receipt" }
8
+ ],
9
+ "$defs": {
10
+ "action": {
11
+ "enum": [
12
+ "runtime.status", "runtime.inspector", "runtime.commands", "orchestration.status",
13
+ "usage.live", "usage.history", "usage.logs", "usage.efficiency", "usage.preflight",
14
+ "onboarding.status", "onboarding.profile", "onboarding.tech",
15
+ "profile.status", "profile.options", "profile.tech-options", "profile.apply", "profile.auto",
16
+ "context.status", "context.rebuild", "context.search", "context.pack", "context.impact", "context.efficiency", "context.preflight", "context.compact",
17
+ "memory.status",
18
+ "mcp.status", "mcp.doctor", "mcp.detail", "mcp.approve", "mcp.reject", "mcp.reset"
19
+ ]
20
+ },
21
+ "effect": { "enum": ["read-only", "workspace-write", "model-assisted"] },
22
+ "output": {
23
+ "type": "object",
24
+ "additionalProperties": false,
25
+ "required": ["customType", "content", "truncated", "redacted"],
26
+ "properties": {
27
+ "customType": { "type": "string", "minLength": 1, "maxLength": 120, "pattern": "^[A-Za-z0-9._-]+$" },
28
+ "content": { "type": "string", "maxLength": 12000, "pattern": "^[^\\u0000]*$" },
29
+ "truncated": { "type": "boolean" },
30
+ "redacted": { "type": "boolean" }
31
+ }
32
+ },
33
+ "command": {
34
+ "type": "object",
35
+ "additionalProperties": false,
36
+ "required": ["schemaVersion", "version", "messageType", "requestId", "sessionRef", "expectedSessionRevision", "action", "argument", "confirmed"],
37
+ "properties": {
38
+ "schemaVersion": { "const": 1 },
39
+ "version": { "const": "piagent-runtime-command-v1" },
40
+ "messageType": { "const": "command" },
41
+ "requestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
42
+ "sessionRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
43
+ "expectedSessionRevision": { "$ref": "common-v1.schema.json#/$defs/revisionToken" },
44
+ "action": { "$ref": "#/$defs/action" },
45
+ "argument": { "type": ["string", "null"], "maxLength": 2048, "pattern": "^[^\\u0000]*$" },
46
+ "confirmed": { "type": "boolean" }
47
+ }
48
+ },
49
+ "receipt": {
50
+ "type": "object",
51
+ "additionalProperties": false,
52
+ "required": ["schemaVersion", "version", "messageType", "requestId", "sessionRef", "action", "state", "resultCode", "effect", "modelCallObserved", "outputs", "sessionRevisionAfter", "reasonCode"],
53
+ "properties": {
54
+ "schemaVersion": { "const": 1 },
55
+ "version": { "const": "piagent-runtime-receipt-v1" },
56
+ "messageType": { "const": "receipt" },
57
+ "requestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
58
+ "sessionRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
59
+ "action": { "$ref": "#/$defs/action" },
60
+ "state": { "enum": ["settled", "rejected", "uncertain"] },
61
+ "resultCode": { "enum": ["completed", "stale-revision", "confirmation-required", "runtime-busy", "invalid-command", "effect-unknown", "unavailable"] },
62
+ "effect": { "$ref": "#/$defs/effect" },
63
+ "modelCallObserved": { "type": "boolean" },
64
+ "outputs": { "type": "array", "maxItems": 8, "items": { "$ref": "#/$defs/output" } },
65
+ "sessionRevisionAfter": { "$ref": "common-v1.schema.json#/$defs/nullableRevisionToken" },
66
+ "reasonCode": { "$ref": "common-v1.schema.json#/$defs/nullableReasonCode" }
67
+ }
68
+ }
69
+ }
70
+ }
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "thinkingLevel": { "enum": ["off", "minimal", "low", "medium", "high", "xhigh", "max"] },
17
17
  "permissionMode": { "enum": ["read-only", "workspace-write", "trusted-full-access"] },
18
+ "workflow": { "enum": ["task", "scout", "be-to-fe", "discuss", "plan", "review", "commit", "pr", "onboard", "platform-improve"] },
18
19
  "baseCommandProperties": {
19
20
  "type": "object",
20
21
  "additionalProperties": false,
@@ -55,6 +56,8 @@
55
56
  "placeRef": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
56
57
  "modelRef": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" },
57
58
  "thinkingLevel": { "$ref": "#/$defs/thinkingLevel" },
59
+ "permissionMode": { "$ref": "#/$defs/permissionMode" },
60
+ "workflow": { "$ref": "#/$defs/workflow" },
58
61
  "message": { "type": "string", "minLength": 1, "maxLength": 32768, "pattern": "^[^\\u0000]*$" },
59
62
  "messageRequestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
60
63
  "deferInitialMessage": {
@@ -82,6 +85,7 @@
82
85
  "required": ["delivery", "message", "messageRequestId", "expectedOperationRef", "attachmentRefs"],
83
86
  "properties": {
84
87
  "delivery": { "enum": ["new-operation", "follow-up", "steer"] },
88
+ "workflow": { "$ref": "#/$defs/workflow" },
85
89
  "message": { "type": "string", "minLength": 1, "maxLength": 32768, "pattern": "^[^\\u0000]*$" },
86
90
  "messageRequestId": { "$ref": "common-v1.schema.json#/$defs/opaqueRef" },
87
91
  "expectedOperationRef": { "$ref": "common-v1.schema.json#/$defs/nullableOpaqueRef" },
@@ -101,6 +105,10 @@
101
105
  {
102
106
  "if": { "properties": { "delivery": { "const": "new-operation" } }, "required": ["delivery"] },
103
107
  "then": { "properties": { "expectedOperationRef": { "type": "null" } } }
108
+ },
109
+ {
110
+ "if": { "properties": { "workflow": { "$ref": "#/$defs/workflow" } }, "required": ["workflow"] },
111
+ "then": { "properties": { "delivery": { "const": "new-operation" } } }
104
112
  }
105
113
  ]
106
114
  }