@piagent/platform 1.5.5 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +117 -0
- package/README.md +9 -8
- package/README.vi.md +9 -5
- package/adapters/web-frontend/profile.json +2 -4
- package/architecture/layers.json +18 -3
- package/benchmarks/deep-logic-v1/SPECIALIST_PROTOCOL.vi.md +122 -0
- package/benchmarks/deep-logic-v1/grade.mjs +272 -10
- package/benchmarks/deep-logic-v1/project/apps/admin/src/billing-summary.js +3 -0
- package/benchmarks/deep-logic-v1/project/benchmark-contract/public-smoke.test.js +166 -0
- package/benchmarks/deep-logic-v1/project/package.json +1 -1
- package/benchmarks/deep-logic-v1/project/packages/billing/src/close-period.js +3 -0
- package/benchmarks/deep-logic-v1/project/packages/billing/src/plan-timeline.js +3 -0
- package/benchmarks/deep-logic-v1/project/test/smoke.test.js +7 -1
- package/benchmarks/deep-logic-v1/prompts/budgeted-context-graph.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/fair-dependency-scheduler.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/layered-policy-resolution.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/resumable-stream-assembly.md +3 -1
- package/benchmarks/deep-logic-v1/prompts/revision-event-reconciliation.md +2 -0
- package/benchmarks/deep-logic-v1/prompts/temporal-usage-billing-close.md +123 -0
- package/benchmarks/deep-logic-v1/prompts/transactional-config-merge.md +2 -0
- package/benchmarks/deep-logic-v1/suite.json +51 -3
- package/benchmarks/deep-logic-v1/variant.mjs +22 -1
- package/benchmarks/production-v1/spend-control.v1.json +68 -0
- package/benchmarks/production-v1/suite.json +42 -3
- package/catalog/capabilities.json +6 -6
- package/docs/command-reference-vietnamese.md +6 -6
- package/docs/context-engine.md +7 -1
- package/docs/distribution-standard.md +7 -7
- package/docs/en/architecture.md +1 -0
- package/docs/languages.json +4 -4
- package/docs/model-options.md +8 -5
- package/docs/operator-manual-vietnamese.md +13 -11
- package/docs/project-adapters.md +2 -2
- package/docs/publishing-for-teams.md +5 -0
- package/docs/quality-benchmark.md +271 -62
- package/docs/release-install-policy.md +10 -9
- package/docs/runtime-policy-design.md +4 -4
- package/docs/runtime-quality-baseline.md +13 -6
- package/docs/security-threat-model.md +2 -2
- package/docs/task-lifecycle-vietnamese.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs/usage-observability.md +7 -2
- package/docs/vi/architecture.md +1 -0
- package/docs-site/architecture.html +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/context-engine.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/index.html +1 -1
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +1 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/whats-new.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +1 -1
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +1 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/whats-new.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.invalid.json +1 -1
- package/evals/fixtures/benchmark-assurance-evidence.valid.json +1 -1
- package/evals/fixtures/piagent-webui/session-live-state-v1.invalid.json +18 -0
- package/evals/fixtures/piagent-webui/session-live-state-v1.valid.json +27 -0
- package/evals/fs4-readiness-matrix.v1.json +2 -2
- package/evals/fs5-causal-arm.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v1.json +2 -2
- package/evals/fs5-pilot-protocol.v4.json +6 -5
- package/evals/fs5-pilot-protocol.v5.json +7 -6
- package/evals/private-holdout-v1/public-exposure.v1.json +1 -1
- package/package.json +3 -1
- package/packages/piagent-core/benchmark/benchmark-assurance.js +4 -1
- package/packages/piagent-core/benchmark/benchmark-bootstrap.js +91 -9
- package/packages/piagent-core/benchmark/benchmark-claim-restrictions.js +38 -7
- package/packages/piagent-core/benchmark/benchmark-cli.js +7 -7
- package/packages/piagent-core/benchmark/benchmark-codex.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-comparison.js +271 -2
- package/packages/piagent-core/benchmark/benchmark-core.js +732 -661
- package/packages/piagent-core/benchmark/benchmark-execution-guard.js +7 -5
- package/packages/piagent-core/benchmark/benchmark-forensics.js +439 -4
- package/packages/piagent-core/benchmark/benchmark-host-readiness.js +639 -0
- package/packages/piagent-core/benchmark/benchmark-matrix.js +22 -4
- package/packages/piagent-core/benchmark/benchmark-normalized-cost.js +317 -0
- package/packages/piagent-core/benchmark/benchmark-preflight.js +3 -1
- package/packages/piagent-core/benchmark/benchmark-process.js +6 -5
- package/packages/piagent-core/benchmark/benchmark-provider-wire.js +126 -0
- package/packages/piagent-core/benchmark/benchmark-record-validation.js +183 -0
- package/packages/piagent-core/benchmark/benchmark-report.js +116 -12
- package/packages/piagent-core/benchmark/benchmark-resume-recovery.js +14 -0
- package/packages/piagent-core/benchmark/benchmark-runner-policy.js +26 -0
- package/packages/piagent-core/benchmark/benchmark-stage-diagnostic.js +782 -0
- package/packages/piagent-core/benchmark/benchmark-statistics.js +64 -0
- package/packages/piagent-core/benchmark/benchmark-stop-policy.js +8 -5
- package/packages/piagent-core/benchmark/benchmark-suite-identity.js +31 -0
- package/packages/piagent-core/benchmark/benchmark-suite-runtime.js +13 -9
- package/packages/piagent-core/benchmark/benchmark-suite.js +128 -2
- package/packages/piagent-core/benchmark/benchmark-summary-support.js +376 -0
- package/packages/piagent-core/benchmark/benchmark-timing-diagnostics.js +971 -0
- package/packages/piagent-core/benchmark/benchmark-usage.js +311 -0
- package/packages/piagent-core/benchmark/benchmark-workflow.js +166 -0
- package/packages/piagent-core/docs/context-efficiency-improvement-plan.md +1080 -0
- package/packages/piagent-core/extensions/acceptance-contract-semantics.js +83 -69
- package/packages/piagent-core/extensions/acceptance-error-classes.js +160 -0
- package/packages/piagent-core/extensions/acceptance-executable-evidence.js +12 -9
- package/packages/piagent-core/extensions/acceptance-receipt.js +34 -11
- package/packages/piagent-core/extensions/acceptance-state-machine-evidence.js +518 -0
- package/packages/piagent-core/extensions/acceptance-test-binding-evidence.js +60 -0
- package/packages/piagent-core/extensions/context-efficiency-metrics.js +302 -0
- package/packages/piagent-core/extensions/context-efficiency-report.js +167 -0
- package/packages/piagent-core/extensions/context-engine.js +148 -145
- package/packages/piagent-core/extensions/context-evidence.js +48 -0
- package/packages/piagent-core/extensions/context-import-links.js +296 -0
- package/packages/piagent-core/extensions/context-telemetry-integrity.js +65 -0
- package/packages/piagent-core/extensions/criterion-context-pack.js +422 -7
- package/packages/piagent-core/extensions/criterion-graph.js +78 -8
- package/packages/piagent-core/extensions/guard-types.ts +2 -0
- package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +233 -41
- package/packages/piagent-core/extensions/task-contract-view.js +17 -3
- package/packages/piagent-core/extensions/task-lifecycle.js +7 -2
- package/packages/piagent-core/extensions/task-state.js +40 -134
- package/packages/piagent-core/extensions/verification-intelligence.js +3 -1
- package/packages/piagent-core/extensions/workspace-evidence-roots.js +157 -0
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/policies/base-policy.json +2 -0
- package/packages/piagent-core/runtime/context/context-delivery.ts +103 -0
- package/packages/piagent-core/runtime/context/context-delta-shadow.ts +53 -0
- package/packages/piagent-core/runtime/context/context-efficiency-format.ts +47 -0
- package/packages/piagent-core/runtime/context/context-evidence-qualification.ts +32 -0
- package/packages/piagent-core/runtime/context/prefix-telemetry.ts +47 -0
- package/packages/piagent-core/runtime/context/repository-memory-hints.ts +27 -0
- package/packages/piagent-core/runtime/hooks/agent-prompt-telemetry.ts +56 -0
- package/packages/piagent-core/runtime/hooks/agent-start-hook.ts +162 -102
- package/packages/piagent-core/runtime/hooks/completion-hook.ts +5 -6
- package/packages/piagent-core/runtime/hooks/input-hook.ts +11 -1
- package/packages/piagent-core/runtime/hooks/session-hooks.ts +22 -2
- package/packages/piagent-core/runtime/hooks/session-start-hook.ts +7 -2
- package/packages/piagent-core/runtime/hooks/tool-result-content-guards.ts +95 -0
- package/packages/piagent-core/runtime/hooks/tool-result-hook.ts +84 -102
- package/packages/piagent-core/runtime/inspection/activity-event-adapter.ts +4 -2
- package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
- package/packages/piagent-core/runtime/inspection/task-compaction-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/task-handoff-history.ts +1 -1
- package/packages/piagent-core/runtime/inspection/tool-failure-classification.ts +53 -0
- package/packages/piagent-core/runtime/inspection/webui-snapshot.ts +34 -6
- package/packages/piagent-core/runtime/model/authenticated-catalog.ts +17 -9
- package/packages/piagent-core/runtime/model/openai-codex-reasoning.ts +83 -0
- package/packages/piagent-core/runtime/model/provider-wire-fingerprint.ts +238 -0
- package/packages/piagent-core/runtime/orchestration/role-policy.ts +1 -1
- package/packages/piagent-core/runtime/product/activity-inspector.ts +7 -2
- package/packages/piagent-core/runtime/product/efficiency-metrics.ts +4 -2
- package/packages/piagent-core/runtime/quality/exact-output-contract.ts +2 -1
- package/packages/piagent-core/runtime/quality/performance-assurance-signals.ts +23 -0
- package/packages/piagent-core/runtime/quality/performance-assurance.ts +6 -2
- package/packages/piagent-core/runtime/recovery/edit-recovery-context.ts +113 -0
- package/packages/piagent-core/runtime/recovery/edit-recovery-delivery.ts +42 -0
- package/packages/piagent-core/runtime/recovery/semantic-repair-handshake.ts +55 -2
- package/packages/piagent-core/runtime/recovery/semantic-repair-runtime.ts +55 -2
- package/packages/piagent-core/runtime/registration/context-commands.ts +66 -5
- package/packages/piagent-core/runtime/registration/policy-tools.ts +46 -11
- package/packages/piagent-core/runtime/registration/task-completion-tools.ts +49 -16
- package/packages/piagent-core/runtime/registration/task-start-guidance.ts +163 -0
- package/packages/piagent-core/runtime/registration/task-start-retry-helpers.ts +37 -0
- package/packages/piagent-core/runtime/registration/task-start-tool.ts +67 -62
- package/packages/piagent-core/runtime/session/runtime-state.ts +154 -2
- package/packages/piagent-core/runtime/session/system-prompt.ts +4 -4
- package/packages/piagent-core/runtime/tools/apply-patch-tool.ts +500 -0
- package/packages/piagent-core/runtime/trajectory/trajectory-runtime.ts +2 -1
- package/packages/piagent-core/runtime/workflows/task-intake.ts +43 -2
- package/packages/piagent-core/security/sensitive-data.js +13 -218
- package/packages/piagent-core/security/sensitive-project-file.js +42 -0
- package/packages/piagent-core/security/sensitive-source-expression.js +330 -0
- package/packages/piagent-core/security/sensitive-source-formats.js +394 -0
- package/packages/piagent-core/security/sensitive-text.js +234 -0
- package/packages/piagent-core/subagents/piagent-worker.md +1 -1
- package/packages/piagent-webui/dist/client/assets/{MarkdownMessage-rD9WJKxs.js → MarkdownMessage-CxeZqTte.js} +1 -1
- package/packages/piagent-webui/dist/client/assets/index-DynIMe8I.js +176 -0
- package/packages/piagent-webui/dist/client/index.html +1 -1
- package/packages/piagent-webui/gateway/gateway-events.ts +109 -5
- package/packages/piagent-webui/gateway/gateway-service.ts +3 -0
- package/packages/piagent-webui/gateway/gateway-session-stream.ts +93 -7
- package/packages/piagent-webui/gateway/session-command-controller.ts +18 -3
- package/packages/piagent-webui/gateway/session-effective-options.ts +47 -0
- package/packages/piagent-webui/gateway/session-live-state.ts +21 -0
- package/packages/piagent-webui/gateway/session-operation-start.ts +5 -1
- package/packages/piagent-webui/gateway/session-operation-watchdog.ts +158 -0
- package/packages/piagent-webui/gateway/session-runtime-factory.ts +45 -0
- package/packages/piagent-webui/gateway/session-runtime-supervisor.ts +139 -124
- package/packages/piagent-webui/server/loopback-server.ts +8 -0
- package/packages/piagent-webui/server/transcript-projection.ts +22 -4
- package/packages/piagent-webui/shared/text-visibility.ts +10 -0
- package/schemas/piagent-webui/catalog-v1.json +9 -0
- package/schemas/piagent-webui/gateway-protocol-v1.schema.json +53 -3
- package/schemas/piagent-webui/session-live-state-v1.schema.json +76 -0
- package/schemas/task-contract.schema.json +12 -1
- package/scripts/benchmark-runner-core.mjs +355 -359
- package/scripts/benchmark-runner-finalization.mjs +309 -0
- package/scripts/benchmark-runner-host-readiness.mjs +94 -0
- package/scripts/benchmark-runner-support.mjs +200 -0
- package/scripts/benchmark-session.mjs +95 -41
- package/scripts/configure-model-scope.sh +4 -3
- package/scripts/helper-evaluation.mjs +1 -1
- package/scripts/install-global.sh +2 -2
- package/scripts/pi-usage-history.mjs +37 -10
- package/scripts/setup.sh +2 -2
- package/scripts/verify-local.sh +5 -1
- package/templates/global/settings.json +2 -1
- package/packages/piagent-webui/dist/client/assets/index-BZRFw1u6.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-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-DynIMe8I.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-BV-_03yl.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
|
|
2
|
+
|
|
1
3
|
export type GatewayEventKind = "catalog.changed" | "session.changed" | "runtime.changed" | "message.delta"
|
|
2
|
-
| "message.completed" | "tool.started" | "tool.completed";
|
|
4
|
+
| "message.completed" | "operation.settled" | "tool.started" | "tool.completed";
|
|
3
5
|
|
|
4
6
|
export type GatewayProtocolEvent = {
|
|
5
7
|
schemaVersion: 1;
|
|
@@ -12,16 +14,47 @@ export type GatewayProtocolEvent = {
|
|
|
12
14
|
payload: Record<string, unknown>;
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
export type GatewayTerminalSettlement = {
|
|
18
|
+
sessionRef: string;
|
|
19
|
+
operationRef: string;
|
|
20
|
+
settlement: "blocked" | "aborted" | "error" | "unknown";
|
|
21
|
+
reasonCode: string;
|
|
22
|
+
settledAt: string;
|
|
23
|
+
sequence: number;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const OPAQUE_REF_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._~-]{0,159}$/;
|
|
27
|
+
const REASON_CODE_PATTERN = /^[a-z0-9][a-z0-9.-]{0,95}$/;
|
|
28
|
+
const TERMINAL_SETTLEMENTS = new Set(["completed", "blocked", "aborted", "error", "unknown"]);
|
|
29
|
+
const SETTLEMENT_RETENTION_COUNT = 100;
|
|
30
|
+
const SETTLEMENT_RETENTION_MS = 60 * 60_000;
|
|
31
|
+
const MAX_SETTLEMENT_DECISION_COUNT = 10_000;
|
|
32
|
+
type GatewaySettlementDecision = { settledAt: string; settlement: "completed" | GatewayTerminalSettlement["settlement"];
|
|
33
|
+
reasonCode: string | null };
|
|
34
|
+
|
|
35
|
+
function settlementReason(settlement: GatewayTerminalSettlement["settlement"], value: unknown): string {
|
|
36
|
+
if (typeof value === "string" && REASON_CODE_PATTERN.test(value) && !redactSensitiveText(value).redacted) return value;
|
|
37
|
+
if (settlement === "aborted") return "operation-aborted";
|
|
38
|
+
if (settlement === "blocked") return "operation-blocked";
|
|
39
|
+
if (settlement === "error") return "operation-failed";
|
|
40
|
+
return "operation-settlement-unknown";
|
|
41
|
+
}
|
|
42
|
+
|
|
15
43
|
export class GatewayEventStore {
|
|
16
44
|
readonly #maximumCount: number;
|
|
17
45
|
readonly #maximumAgeMs: number;
|
|
46
|
+
readonly #settlementDecisionCount: number;
|
|
18
47
|
#events: GatewayProtocolEvent[] = [];
|
|
48
|
+
#settlements: GatewayTerminalSettlement[] = [];
|
|
49
|
+
#settlementDecisions = new Map<string, GatewaySettlementDecision>();
|
|
19
50
|
#sequence = 0;
|
|
20
51
|
#listeners = new Set<(event: GatewayProtocolEvent) => void>();
|
|
21
52
|
|
|
22
|
-
constructor(options: { maximumCount?: number; maximumAgeMs?: number } = {}) {
|
|
53
|
+
constructor(options: { maximumCount?: number; maximumAgeMs?: number; settlementDecisionCount?: number } = {}) {
|
|
23
54
|
this.#maximumCount = Math.max(1, Math.min(10_000, options.maximumCount ?? 1_000));
|
|
24
55
|
this.#maximumAgeMs = Math.max(1_000, Math.min(24 * 60 * 60_000, options.maximumAgeMs ?? 60 * 60_000));
|
|
56
|
+
this.#settlementDecisionCount = Math.max(1,
|
|
57
|
+
Math.min(MAX_SETTLEMENT_DECISION_COUNT, Math.floor(options.settlementDecisionCount ?? MAX_SETTLEMENT_DECISION_COUNT)));
|
|
25
58
|
}
|
|
26
59
|
|
|
27
60
|
get stateVersion(): number { return this.#sequence; }
|
|
@@ -31,14 +64,74 @@ export class GatewayEventStore {
|
|
|
31
64
|
while (this.#events.length > this.#maximumCount || this.#events[0] && Date.parse(this.#events[0].generatedAt) < cutoff) this.#events.shift();
|
|
32
65
|
}
|
|
33
66
|
|
|
67
|
+
#pruneSettlements(now: number): void {
|
|
68
|
+
const cutoff = now - SETTLEMENT_RETENTION_MS;
|
|
69
|
+
this.#settlements = this.#settlements.filter((item) => Date.parse(item.settledAt) >= cutoff);
|
|
70
|
+
if (this.#settlements.length > SETTLEMENT_RETENTION_COUNT) {
|
|
71
|
+
this.#settlements.splice(0, this.#settlements.length - SETTLEMENT_RETENTION_COUNT);
|
|
72
|
+
}
|
|
73
|
+
for (const [key, decision] of this.#settlementDecisions) {
|
|
74
|
+
if (Date.parse(decision.settledAt) < cutoff) this.#settlementDecisions.delete(key);
|
|
75
|
+
}
|
|
76
|
+
while (this.#settlementDecisions.size > this.#settlementDecisionCount) {
|
|
77
|
+
this.#settlementDecisions.delete(this.#settlementDecisions.keys().next().value as string);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
#settlementDecision(sessionRef: string, operationRef: string): GatewaySettlementDecision | undefined {
|
|
82
|
+
const key = `${sessionRef}\u0000${operationRef}`;
|
|
83
|
+
const indexed = this.#settlementDecisions.get(key);
|
|
84
|
+
if (indexed) return indexed;
|
|
85
|
+
const retained = this.#settlements.find((item) => item.sessionRef === sessionRef && item.operationRef === operationRef);
|
|
86
|
+
if (!retained) return undefined;
|
|
87
|
+
const restored = { settledAt: retained.settledAt, settlement: retained.settlement, reasonCode: retained.reasonCode };
|
|
88
|
+
this.#settlementDecisions.set(key, restored);
|
|
89
|
+
return restored;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
#recordSettlement(event: GatewayProtocolEvent, now: number): void {
|
|
93
|
+
if (event.kind !== "operation.settled") return;
|
|
94
|
+
const sessionRef = event.payload.sessionRef, operationRef = event.payload.operationRef, settlement = event.payload.settlement;
|
|
95
|
+
if (typeof sessionRef !== "string" || !OPAQUE_REF_PATTERN.test(sessionRef)
|
|
96
|
+
|| typeof operationRef !== "string" || !OPAQUE_REF_PATTERN.test(operationRef)
|
|
97
|
+
|| typeof settlement !== "string" || !TERMINAL_SETTLEMENTS.has(settlement)) return;
|
|
98
|
+
this.#pruneSettlements(now);
|
|
99
|
+
const key = `${sessionRef}\u0000${operationRef}`;
|
|
100
|
+
if (this.#settlementDecision(sessionRef, operationRef)) return;
|
|
101
|
+
this.#settlementDecisions.set(key, { settledAt: event.generatedAt,
|
|
102
|
+
settlement: settlement as GatewaySettlementDecision["settlement"],
|
|
103
|
+
reasonCode: settlement === "completed" ? null : String(event.payload.reasonCode) });
|
|
104
|
+
const retained = { sessionRef, operationRef, settledAt: event.generatedAt, sequence: event.sequence };
|
|
105
|
+
if (settlement !== "completed") {
|
|
106
|
+
const outcome = settlement as GatewayTerminalSettlement["settlement"];
|
|
107
|
+
this.#settlements.push({ ...retained, settlement: outcome, reasonCode: settlementReason(outcome, event.payload.reasonCode) });
|
|
108
|
+
}
|
|
109
|
+
this.#pruneSettlements(now);
|
|
110
|
+
}
|
|
111
|
+
|
|
34
112
|
publish(kind: GatewayEventKind, payload: Record<string, unknown>, now = new Date()): GatewayProtocolEvent {
|
|
35
113
|
if (!Number.isFinite(now.getTime())) throw new Error("gateway-event-time-invalid");
|
|
114
|
+
if (kind === "operation.settled") {
|
|
115
|
+
if (payload.settlement === "completed") payload = { ...payload, reasonCode: null };
|
|
116
|
+
else {
|
|
117
|
+
const settlement = ["blocked", "aborted", "error", "unknown"].includes(String(payload.settlement))
|
|
118
|
+
? payload.settlement as GatewayTerminalSettlement["settlement"] : "unknown";
|
|
119
|
+
payload = { ...payload, settlement, reasonCode: settlementReason(settlement, payload.reasonCode) };
|
|
120
|
+
}
|
|
121
|
+
this.#pruneSettlements(now.getTime());
|
|
122
|
+
const sessionRef = payload.sessionRef, operationRef = payload.operationRef;
|
|
123
|
+
if (typeof sessionRef === "string" && OPAQUE_REF_PATTERN.test(sessionRef)
|
|
124
|
+
&& typeof operationRef === "string" && OPAQUE_REF_PATTERN.test(operationRef)) {
|
|
125
|
+
const canonical = this.#settlementDecision(sessionRef, operationRef);
|
|
126
|
+
if (canonical) payload = { ...payload, settlement: canonical.settlement, reasonCode: canonical.reasonCode };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
36
129
|
this.#sequence += 1;
|
|
37
130
|
const event: GatewayProtocolEvent = {
|
|
38
131
|
schemaVersion: 1, version: "piagent-gateway-protocol-v1", messageType: "event",
|
|
39
132
|
sequence: this.#sequence, stateVersion: this.#sequence, generatedAt: now.toISOString(), kind, payload
|
|
40
133
|
};
|
|
41
|
-
this.#events.push(event); this.#prune(now.getTime());
|
|
134
|
+
this.#events.push(event); this.#prune(now.getTime()); this.#recordSettlement(event, now.getTime());
|
|
42
135
|
for (const listener of this.#listeners) { try { listener(event); } catch { /* observers cannot block truth */ } }
|
|
43
136
|
return event;
|
|
44
137
|
}
|
|
@@ -48,10 +141,21 @@ export class GatewayEventStore {
|
|
|
48
141
|
return () => { this.#listeners.delete(listener); };
|
|
49
142
|
}
|
|
50
143
|
|
|
144
|
+
recentOperationSettlements(maximum = 100, now = new Date()): GatewayTerminalSettlement[] {
|
|
145
|
+
if (!Number.isFinite(now.getTime())) throw new Error("gateway-settlement-time-invalid");
|
|
146
|
+
const limit = Math.max(0, Math.min(100, Math.floor(maximum)));
|
|
147
|
+
if (limit === 0) return [];
|
|
148
|
+
this.#pruneSettlements(now.getTime());
|
|
149
|
+
return [...this.#settlements].sort((left, right) => right.sequence - left.sequence).slice(0, limit).map((item) => ({ ...item }));
|
|
150
|
+
}
|
|
151
|
+
|
|
51
152
|
replay(after: number | null): { state: "current" | "resync-required"; events: GatewayProtocolEvent[]; earliestSequence: number; currentSequence: number } {
|
|
52
153
|
this.#prune(Date.now());
|
|
53
|
-
|
|
54
|
-
|
|
154
|
+
// An empty retained ring after pruning contains no event at `#sequence`.
|
|
155
|
+
// Treat the next sequence as the retention boundary so a client one event
|
|
156
|
+
// behind must resync instead of silently receiving an empty replay.
|
|
157
|
+
const earliestSequence = this.#events[0]?.sequence ?? Math.min(Number.MAX_SAFE_INTEGER, this.#sequence + 1);
|
|
158
|
+
if (after !== null && (after > this.#sequence || after < Math.max(0, earliestSequence - 1))) {
|
|
55
159
|
return { state: "resync-required", events: [], earliestSequence, currentSequence: this.#sequence };
|
|
56
160
|
}
|
|
57
161
|
return { state: "current", events: after === null ? [] : this.#events.filter((event) => event.sequence > after), earliestSequence,
|
|
@@ -21,6 +21,7 @@ import { SessionMetadataStore } from "./session-metadata-store.ts";
|
|
|
21
21
|
import { GatewayProtocolService } from "./gateway-protocol-service.ts";
|
|
22
22
|
import { SessionLeaseStore } from "./session-lease-store.ts";
|
|
23
23
|
import { SessionRuntimeSupervisor } from "./session-runtime-supervisor.ts";
|
|
24
|
+
import { buildSessionLiveState } from "./session-live-state.ts";
|
|
24
25
|
import { SessionCommandStore } from "./session-command-store.ts";
|
|
25
26
|
import { SessionCommandController } from "./session-command-controller.ts";
|
|
26
27
|
import { RuntimeCommandController } from "./runtime-command-controller.ts";
|
|
@@ -192,6 +193,8 @@ export async function startPiagentGateway(options: {
|
|
|
192
193
|
mode: "gateway",
|
|
193
194
|
readCapabilities: () => capabilities(gatewayInstanceRef, true),
|
|
194
195
|
readSessionCatalog: readCatalog,
|
|
196
|
+
readSessionLiveState: () => buildSessionLiveState({ gatewayInstanceRef, eventSequence: events.stateVersion,
|
|
197
|
+
operations: runtimes!.currentOperations(), settlements: events.recentOperationSettlements() }),
|
|
195
198
|
readSessionCreationOptions: () => inspections.creationOptions(),
|
|
196
199
|
readSessionModel: (sessionRef) => inspections.provider(sessionRef),
|
|
197
200
|
readSessionConnections: (sessionRef) => inspections.connections(sessionRef),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { redactSensitiveText } from "../../piagent-core/security/sensitive-data.js";
|
|
4
|
+
import { hasVisibleText } from "../shared/text-visibility.ts";
|
|
4
5
|
import { GatewayEventStore } from "./gateway-events.ts";
|
|
5
6
|
|
|
6
7
|
const MAX_BUFFER = 65_536;
|
|
@@ -8,9 +9,14 @@ const MAX_EVENTS = 128;
|
|
|
8
9
|
const MAX_DELTA = 16_384;
|
|
9
10
|
const PRIVATE_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
|
|
10
11
|
const PRIVATE_END = /-----END [A-Z ]*PRIVATE KEY-----/;
|
|
12
|
+
const RUNTIME_DRIFT = /Installed Piagent runtime changed (?:during this session|while the capability lock was being re-pinned)/i;
|
|
13
|
+
const COMPLETION_GATE = /^\[Piagent completion gate: (CONTINUING|NOT APPROVED)\]/i;
|
|
14
|
+
|
|
15
|
+
export type GatewayOperationSettlement = "completed" | "blocked" | "aborted" | "error" | "unknown";
|
|
16
|
+
type Settlement = { outcome: GatewayOperationSettlement; reasonCode: string | null };
|
|
11
17
|
|
|
12
18
|
function clean(value: unknown): string {
|
|
13
|
-
return String(value ?? "").replace(/\u001b(
|
|
19
|
+
return String(value ?? "").replace(/\u001b(?:\[[0-?]*[ -/]*[@-~]|[@-_])/g, "")
|
|
14
20
|
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
|
|
15
21
|
}
|
|
16
22
|
function safeTool(value: unknown): string {
|
|
@@ -19,6 +25,44 @@ function safeTool(value: unknown): string {
|
|
|
19
25
|
return safe.text.replace(/[^A-Za-z0-9._:-]/g, "-").replace(/^[^A-Za-z0-9]+/, "").slice(0, 160) || "tool";
|
|
20
26
|
}
|
|
21
27
|
function ref(prefix: string): string { return `${prefix}_${randomBytes(18).toString("base64url")}`; }
|
|
28
|
+
function messageText(message: any): string {
|
|
29
|
+
if (typeof message?.content === "string") return message.content;
|
|
30
|
+
if (!Array.isArray(message?.content)) return "";
|
|
31
|
+
return message.content.filter((item: any) => item?.type === "text" && typeof item.text === "string")
|
|
32
|
+
.map((item: any) => item.text).join("\n");
|
|
33
|
+
}
|
|
34
|
+
function assistantSettlement(message: any): Settlement {
|
|
35
|
+
const stopReason = String(message?.stopReason ?? "");
|
|
36
|
+
if (stopReason === "aborted") return { outcome: "aborted", reasonCode: "operation-aborted" };
|
|
37
|
+
if (stopReason === "error") return { outcome: "error", reasonCode: "assistant-response-failed" };
|
|
38
|
+
if (stopReason === "length") return { outcome: "error", reasonCode: "assistant-output-incomplete" };
|
|
39
|
+
const gate = COMPLETION_GATE.exec(messageText(message).trimStart());
|
|
40
|
+
if (gate) return { outcome: "blocked", reasonCode: gate[1]?.toUpperCase() === "CONTINUING"
|
|
41
|
+
? "completion-gate-continuing" : "completion-gate-not-approved" };
|
|
42
|
+
if (stopReason === "stop") return hasVisibleText(redactSensitiveText(clean(messageText(message))).text)
|
|
43
|
+
? { outcome: "completed", reasonCode: null }
|
|
44
|
+
: { outcome: "unknown", reasonCode: "assistant-message-empty" };
|
|
45
|
+
if (["toolUse", "pending", "deferred"].includes(stopReason)) return { outcome: "unknown", reasonCode: "assistant-message-intermediate" };
|
|
46
|
+
return { outcome: "unknown", reasonCode: "operation-settlement-unknown" };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function containsRuntimeDrift(value: unknown, depth = 0, remaining = { value: 16_384 }): boolean {
|
|
50
|
+
if (depth > 5 || remaining.value <= 0 || value === null || value === undefined) return false;
|
|
51
|
+
if (typeof value === "string") {
|
|
52
|
+
const text = value.slice(0, remaining.value); remaining.value -= text.length;
|
|
53
|
+
return RUNTIME_DRIFT.test(text);
|
|
54
|
+
}
|
|
55
|
+
if (Array.isArray(value)) return value.some((item) => containsRuntimeDrift(item, depth + 1, remaining));
|
|
56
|
+
if (typeof value !== "object") return false;
|
|
57
|
+
const record = value as Record<string, unknown>;
|
|
58
|
+
return ["reason", "message", "text", "content", "result", "details"]
|
|
59
|
+
.some((key) => containsRuntimeDrift(record[key], depth + 1, remaining));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function runtimeRestartReasonCode(event: unknown): "runtime-restart-required" | null {
|
|
63
|
+
if (!event || typeof event !== "object" || (event as Record<string, unknown>).isError !== true) return null;
|
|
64
|
+
return containsRuntimeDrift((event as Record<string, unknown>).result) ? "runtime-restart-required" : null;
|
|
65
|
+
}
|
|
22
66
|
|
|
23
67
|
export class GatewaySessionStream {
|
|
24
68
|
readonly sessionRef: string;
|
|
@@ -32,9 +76,28 @@ export class GatewaySessionStream {
|
|
|
32
76
|
#started = false;
|
|
33
77
|
#startListeners: Array<() => void> = [];
|
|
34
78
|
#settled = false;
|
|
79
|
+
#runtimeRestartRequired = false;
|
|
80
|
+
#lastMessageRef: string | null = null;
|
|
81
|
+
#settlement: Settlement = { outcome: "unknown", reasonCode: "operation-settlement-unknown" };
|
|
82
|
+
#forcedSettlement: Settlement | null = null;
|
|
83
|
+
#lifecycleTerminationReasonCode: string | null = null;
|
|
84
|
+
#terminalEmitted = false;
|
|
35
85
|
#settleListeners: Array<() => void> = [];
|
|
36
86
|
readonly #toolRefs = new Map<string, string>();
|
|
37
87
|
|
|
88
|
+
get runtimeRestartRequired(): boolean { return this.#runtimeRestartRequired; }
|
|
89
|
+
get lifecycleTerminationReasonCode(): string | null { return this.#lifecycleTerminationReasonCode; }
|
|
90
|
+
|
|
91
|
+
markAborted(reasonCode = "operation-aborted"): void { this.#forcedSettlement = { outcome: "aborted", reasonCode }; }
|
|
92
|
+
markError(reasonCode = "operation-failed"): void {
|
|
93
|
+
if (!this.#forcedSettlement) this.#forcedSettlement = { outcome: "error", reasonCode };
|
|
94
|
+
}
|
|
95
|
+
forceLifecycleTermination(reasonCode: string): void {
|
|
96
|
+
this.#lifecycleTerminationReasonCode ??= reasonCode;
|
|
97
|
+
if (!this.#started) { this.#started = true; for (const resolve of this.#startListeners.splice(0)) resolve(); }
|
|
98
|
+
if (!this.#settled) { this.#settled = true; for (const resolve of this.#settleListeners.splice(0)) resolve(); }
|
|
99
|
+
}
|
|
100
|
+
|
|
38
101
|
constructor(options: { sessionRef: string; operationRef: string; events: GatewayEventStore }) {
|
|
39
102
|
this.sessionRef = options.sessionRef; this.operationRef = options.operationRef; this.#events = options.events;
|
|
40
103
|
}
|
|
@@ -50,6 +113,7 @@ export class GatewaySessionStream {
|
|
|
50
113
|
}
|
|
51
114
|
|
|
52
115
|
observe(event: any): void {
|
|
116
|
+
if (this.#terminalEmitted) return;
|
|
53
117
|
if (event?.type === "agent_start") {
|
|
54
118
|
this.#started = true; for (const resolve of this.#startListeners.splice(0)) resolve(); return;
|
|
55
119
|
}
|
|
@@ -67,17 +131,21 @@ export class GatewaySessionStream {
|
|
|
67
131
|
if (event.assistantMessageEvent.delta.length > remaining) this.#truncated = true;
|
|
68
132
|
this.#flush(false); return;
|
|
69
133
|
}
|
|
70
|
-
if (event?.type === "message_end" && event.message?.role === "assistant") {
|
|
134
|
+
if (event?.type === "message_end" && event.message?.role === "assistant") {
|
|
135
|
+
this.#flush(true); this.#lastMessageRef = this.#messageRef; this.#settlement = assistantSettlement(event.message); return;
|
|
136
|
+
}
|
|
71
137
|
if (event?.type === "tool_execution_start" && typeof event.toolCallId === "string") {
|
|
72
138
|
const toolCallRef = ref("tool"); this.#toolRefs.set(event.toolCallId, toolCallRef);
|
|
73
139
|
this.#events.publish("tool.started", { sessionRef: this.sessionRef, operationRef: this.operationRef, toolCallRef,
|
|
74
|
-
toolLabel: safeTool(event.toolName), isError: null });
|
|
140
|
+
toolLabel: safeTool(event.toolName), isError: null, reasonCode: null });
|
|
75
141
|
return;
|
|
76
142
|
}
|
|
77
143
|
if (event?.type === "tool_execution_end" && typeof event.toolCallId === "string") {
|
|
78
144
|
const toolCallRef = this.#toolRefs.get(event.toolCallId); if (!toolCallRef) return;
|
|
145
|
+
const reasonCode = runtimeRestartReasonCode(event);
|
|
146
|
+
if (reasonCode) this.#runtimeRestartRequired = true;
|
|
79
147
|
this.#events.publish("tool.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef, toolCallRef,
|
|
80
|
-
toolLabel: safeTool(event.toolName), isError: event.isError === true });
|
|
148
|
+
toolLabel: safeTool(event.toolName), isError: event.isError === true, reasonCode });
|
|
81
149
|
this.#toolRefs.delete(event.toolCallId);
|
|
82
150
|
}
|
|
83
151
|
}
|
|
@@ -112,9 +180,27 @@ export class GatewaySessionStream {
|
|
|
112
180
|
}
|
|
113
181
|
}
|
|
114
182
|
|
|
115
|
-
complete(sessionRevision: string): void {
|
|
183
|
+
complete(sessionRevision: string | null): void {
|
|
184
|
+
if (this.#terminalEmitted) return;
|
|
185
|
+
this.#terminalEmitted = true;
|
|
116
186
|
this.#flush(true);
|
|
117
|
-
|
|
118
|
-
|
|
187
|
+
let settlement = this.#forcedSettlement ?? (this.#runtimeRestartRequired
|
|
188
|
+
? { outcome: "unknown" as const, reasonCode: "runtime-restart-required" }
|
|
189
|
+
: this.#settlement);
|
|
190
|
+
const messageRef = this.#lastMessageRef ?? this.#messageRef;
|
|
191
|
+
if (settlement.outcome === "completed" && !messageRef) {
|
|
192
|
+
settlement = { outcome: "unknown", reasonCode: "assistant-message-unavailable" };
|
|
193
|
+
} else if (settlement.outcome === "completed" && sessionRevision === null) {
|
|
194
|
+
// Streaming text is not durable success until the canonical session
|
|
195
|
+
// projection confirms it. Still terminate the UI, but do not show draft.
|
|
196
|
+
settlement = { outcome: "unknown", reasonCode: "session-projection-unavailable" };
|
|
197
|
+
}
|
|
198
|
+
if (settlement.outcome === "completed" && messageRef && sessionRevision) {
|
|
199
|
+
this.#events.publish("message.completed", { sessionRef: this.sessionRef, operationRef: this.operationRef,
|
|
200
|
+
messageRef, sessionRevision, truncated: this.#truncated });
|
|
201
|
+
}
|
|
202
|
+
this.#events.publish("operation.settled", { sessionRef: this.sessionRef, operationRef: this.operationRef,
|
|
203
|
+
messageRef, sessionRevision, settlement: settlement.outcome,
|
|
204
|
+
reasonCode: settlement.outcome === "completed" ? null : settlement.reasonCode ?? "operation-settlement-unknown" });
|
|
119
205
|
}
|
|
120
206
|
}
|
|
@@ -154,8 +154,18 @@ export class SessionCommandController {
|
|
|
154
154
|
| "renamed" | "pinned" | "unpinned" | "archived" | "unarchived" | "forked" | "no-change",
|
|
155
155
|
operationRef: string | null = null;
|
|
156
156
|
if (command.action === "session.create") {
|
|
157
|
-
|
|
158
|
-
|
|
157
|
+
const createWithReadback = (this.#runtimes as any).createWithReadback;
|
|
158
|
+
if (typeof createWithReadback === "function") {
|
|
159
|
+
const created = await createWithReadback.call(this.#runtimes, String(command.payload.projectRef), String(command.payload.placeRef),
|
|
160
|
+
command.payload.modelRef === null ? null : String(command.payload.modelRef), String(command.payload.thinkingLevel));
|
|
161
|
+
targetSessionRef = String(created.sessionRef);
|
|
162
|
+
if (created.effectiveOptions?.state !== "confirmed") throw new Error(
|
|
163
|
+
typeof created.effectiveOptions?.reasonCode === "string" ? created.effectiveOptions.reasonCode : "session-create-options-effect-unknown");
|
|
164
|
+
} else {
|
|
165
|
+
// Compatibility with older embedded supervisors and test doubles.
|
|
166
|
+
targetSessionRef = await this.#runtimes.create(String(command.payload.projectRef), String(command.payload.placeRef),
|
|
167
|
+
command.payload.modelRef === null ? null : String(command.payload.modelRef), String(command.payload.thinkingLevel));
|
|
168
|
+
}
|
|
159
169
|
if (command.payload.permissionMode !== undefined) {
|
|
160
170
|
await this.#runtimes.setPermission(targetSessionRef, String(command.payload.permissionMode) as
|
|
161
171
|
"read-only" | "workspace-write" | "trusted-full-access");
|
|
@@ -246,9 +256,14 @@ export class SessionCommandController {
|
|
|
246
256
|
const code = /recovery/.test(message) ? "recovery-required"
|
|
247
257
|
: /(owner-conflict|operation-conflict|runtime-busy)/.test(message) ? "owner-conflict"
|
|
248
258
|
: noDispatchEffect || command.action === "session.send" && !sendAttempted ? "unavailable" : "effect-unknown";
|
|
249
|
-
const
|
|
259
|
+
const specificCreateUncertainty = command.action === "session.create" && /^[a-z0-9][a-z0-9.-]{0,95}$/.test(message)
|
|
260
|
+
? message : "session-command-effect-unknown";
|
|
261
|
+
const receipt = code === "effect-unknown" ? this.#uncertain(command, after, afterRow, specificCreateUncertainty)
|
|
250
262
|
: this.#rejected(command, after, afterRow, code, message);
|
|
251
263
|
try { this.#store.settle(command, receipt, this.#now()); } catch { /* intent remains uncertain */ }
|
|
264
|
+
if (command.action === "session.create" && afterRow) {
|
|
265
|
+
this.#events.publish("session.changed", { catalogRevision: after.catalogRevision, session: afterRow });
|
|
266
|
+
}
|
|
252
267
|
return receipt;
|
|
253
268
|
}
|
|
254
269
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { webUiModelRef } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
|
|
2
|
+
|
|
3
|
+
export type EffectiveSessionOptions = {
|
|
4
|
+
state: "confirmed" | "mismatch" | "unknown";
|
|
5
|
+
modelRef: string | null;
|
|
6
|
+
thinkingLevel: string | null;
|
|
7
|
+
reasonCode: string | null;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const THINKING_LEVELS = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
|
|
11
|
+
|
|
12
|
+
export function effectiveModelRef(session: any): string | null {
|
|
13
|
+
const model = session?.model;
|
|
14
|
+
if (!model || typeof model !== "object") return null;
|
|
15
|
+
const provider = String(model.provider ?? ""), id = String(model.id ?? model.modelId ?? "");
|
|
16
|
+
return provider && id ? webUiModelRef(provider, id) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function effectiveThinkingLevel(session: any): string | null {
|
|
20
|
+
let value: unknown;
|
|
21
|
+
try { value = typeof session?.getThinkingLevel === "function" ? session.getThinkingLevel() : undefined; }
|
|
22
|
+
catch { return null; }
|
|
23
|
+
if (value === undefined || value === null || value === "") value = session?.thinkingLevel;
|
|
24
|
+
const level = String(value ?? "");
|
|
25
|
+
return THINKING_LEVELS.has(level) ? level : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function configureSessionOptions(session: any, modelRef: string | null,
|
|
29
|
+
thinkingLevel: string): Promise<EffectiveSessionOptions> {
|
|
30
|
+
if (modelRef) {
|
|
31
|
+
const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
|
|
32
|
+
const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""),
|
|
33
|
+
String(value.id ?? value.modelId ?? "")) === modelRef);
|
|
34
|
+
try { if (model) await session.setModel(model); } catch { /* Canonical read-back below decides the effect. */ }
|
|
35
|
+
}
|
|
36
|
+
try { await session.setThinkingLevel(thinkingLevel); } catch { /* Canonical read-back below decides the effect. */ }
|
|
37
|
+
const effectiveModel = effectiveModelRef(session), effectiveThinking = effectiveThinkingLevel(session);
|
|
38
|
+
if (!effectiveModel) return { state: "unknown", modelRef: null,
|
|
39
|
+
thinkingLevel: effectiveThinking, reasonCode: "session-model-effect-unknown" };
|
|
40
|
+
if (!effectiveThinking) return { state: "unknown", modelRef: effectiveModel,
|
|
41
|
+
thinkingLevel: null, reasonCode: "session-thinking-effect-unknown" };
|
|
42
|
+
if (modelRef && effectiveModel !== modelRef) return { state: "mismatch", modelRef: effectiveModel,
|
|
43
|
+
thinkingLevel: effectiveThinking, reasonCode: "session-model-mismatch" };
|
|
44
|
+
if (effectiveThinking !== thinkingLevel) return { state: "mismatch", modelRef: effectiveModel,
|
|
45
|
+
thinkingLevel: effectiveThinking, reasonCode: "session-thinking-mismatch" };
|
|
46
|
+
return { state: "confirmed", modelRef: effectiveModel, thinkingLevel: effectiveThinking, reasonCode: null };
|
|
47
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { PiagentWebUICanonicalVolatileSessionOperationStateV1 } from "../contracts/generated/session-live-state-v1.ts";
|
|
2
|
+
import type { GatewayTerminalSettlement } from "./gateway-events.ts";
|
|
3
|
+
import type { CurrentOperationProjection } from "./session-runtime-supervisor.ts";
|
|
4
|
+
|
|
5
|
+
export function buildSessionLiveState(options: { gatewayInstanceRef: string; eventSequence: number;
|
|
6
|
+
operations: Array<CurrentOperationProjection & { sessionRef: string }>; settlements: GatewayTerminalSettlement[];
|
|
7
|
+
now?: Date }): PiagentWebUICanonicalVolatileSessionOperationStateV1 {
|
|
8
|
+
const now = options.now ?? new Date();
|
|
9
|
+
if (!Number.isFinite(now.getTime())) throw new Error("session-live-state-time-invalid");
|
|
10
|
+
return {
|
|
11
|
+
schemaVersion: 1,
|
|
12
|
+
version: "piagent-session-live-state-v1",
|
|
13
|
+
generatedAt: now.toISOString(),
|
|
14
|
+
gatewayInstanceRef: options.gatewayInstanceRef,
|
|
15
|
+
eventSequence: options.eventSequence,
|
|
16
|
+
state: "ready",
|
|
17
|
+
operations: [...options.operations].sort((left, right) => left.sessionRef.localeCompare(right.sessionRef)),
|
|
18
|
+
settlements: [...options.settlements].sort((left, right) => right.sequence - left.sequence).slice(0, 100),
|
|
19
|
+
reasonCode: null
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -12,7 +12,10 @@ export async function waitForOperationStart(
|
|
|
12
12
|
prompt.then(() => ({ state: "prompt-resolved" as const }),
|
|
13
13
|
(error: unknown) => ({ state: "prompt-rejected" as const, error }))
|
|
14
14
|
]);
|
|
15
|
-
if (first.state === "started")
|
|
15
|
+
if (first.state === "started") {
|
|
16
|
+
if (stream.lifecycleTerminationReasonCode) throw new Error(stream.lifecycleTerminationReasonCode);
|
|
17
|
+
return "prompt-owned";
|
|
18
|
+
}
|
|
16
19
|
if (first.state === "prompt-rejected") throw first.error;
|
|
17
20
|
|
|
18
21
|
// Extension commands such as `/workflow` launch their agent turn through
|
|
@@ -27,6 +30,7 @@ export async function waitForOperationStart(
|
|
|
27
30
|
DEFERRED_OPERATION_START_GRACE_MS);
|
|
28
31
|
})
|
|
29
32
|
]);
|
|
33
|
+
if (stream.lifecycleTerminationReasonCode) throw new Error(stream.lifecycleTerminationReasonCode);
|
|
30
34
|
return "deferred";
|
|
31
35
|
} finally {
|
|
32
36
|
if (timer) clearTimeout(timer);
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
export type SessionOperationDeadlineReason = "operation-inactivity-timeout" | "operation-deadline-exceeded";
|
|
2
|
+
|
|
3
|
+
export type SessionOperationDeadlinePolicy = Readonly<{
|
|
4
|
+
inactivityTimeoutMs: number;
|
|
5
|
+
maximumDurationMs: number;
|
|
6
|
+
terminationTimeoutMs: number;
|
|
7
|
+
projectionTimeoutMs: number;
|
|
8
|
+
}>;
|
|
9
|
+
|
|
10
|
+
export type SessionOperationWatchdogOptions = Partial<SessionOperationDeadlinePolicy>;
|
|
11
|
+
export type BoundedResult<T> = { state: "settled"; value: T } | { state: "rejected"; error: unknown } | { state: "timeout" };
|
|
12
|
+
export type SessionOperationTerminationResult = { state: "settled" } | { state: "quarantine"; reasonCode: string };
|
|
13
|
+
|
|
14
|
+
export function bestEffortUnsubscribe(unsubscribe: (() => void) | null): void {
|
|
15
|
+
try { unsubscribe?.(); } catch { /* broken host cleanup cannot retain operation authority */ }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// A silent tool/provider gets 15 minutes. Any observed host event refreshes that
|
|
19
|
+
// allowance, while the six-hour ceiling prevents an endless stream of progress
|
|
20
|
+
// events from retaining runtime authority forever. Cleanup itself is separately
|
|
21
|
+
// bounded so Stop and watchdog recovery cannot inherit the original hang.
|
|
22
|
+
const DEFAULT_POLICY: SessionOperationDeadlinePolicy = Object.freeze({
|
|
23
|
+
inactivityTimeoutMs: 15 * 60_000,
|
|
24
|
+
maximumDurationMs: 6 * 60 * 60_000,
|
|
25
|
+
terminationTimeoutMs: 5_000,
|
|
26
|
+
projectionTimeoutMs: 2_500
|
|
27
|
+
});
|
|
28
|
+
const MAX_TIMEOUT_MS = 24 * 60 * 60_000;
|
|
29
|
+
const TERMINATION_CLEANUP_FAILED = "operation-termination-cleanup-failed";
|
|
30
|
+
|
|
31
|
+
function duration(value: unknown, fallback: number): number {
|
|
32
|
+
if (value === undefined) return fallback;
|
|
33
|
+
if (!Number.isSafeInteger(value) || Number(value) < 1 || Number(value) > MAX_TIMEOUT_MS) {
|
|
34
|
+
throw new Error("session-operation-watchdog-config-invalid");
|
|
35
|
+
}
|
|
36
|
+
return Number(value);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function sessionOperationDeadlinePolicy(options: SessionOperationWatchdogOptions = {}): SessionOperationDeadlinePolicy {
|
|
40
|
+
const policy = {
|
|
41
|
+
inactivityTimeoutMs: duration(options.inactivityTimeoutMs, DEFAULT_POLICY.inactivityTimeoutMs),
|
|
42
|
+
maximumDurationMs: duration(options.maximumDurationMs, DEFAULT_POLICY.maximumDurationMs),
|
|
43
|
+
terminationTimeoutMs: duration(options.terminationTimeoutMs, DEFAULT_POLICY.terminationTimeoutMs),
|
|
44
|
+
projectionTimeoutMs: duration(options.projectionTimeoutMs, DEFAULT_POLICY.projectionTimeoutMs)
|
|
45
|
+
};
|
|
46
|
+
if (policy.projectionTimeoutMs > policy.terminationTimeoutMs) throw new Error("session-operation-watchdog-config-invalid");
|
|
47
|
+
return Object.freeze(policy);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function boundedResult<T>(promise: PromiseLike<T>, timeoutMs: number): Promise<BoundedResult<T>> {
|
|
51
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
52
|
+
try {
|
|
53
|
+
return await Promise.race([
|
|
54
|
+
Promise.resolve(promise).then((value) => ({ state: "settled" as const, value }),
|
|
55
|
+
(error: unknown) => ({ state: "rejected" as const, error })),
|
|
56
|
+
new Promise<{ state: "timeout" }>((resolve) => { timer = setTimeout(() => resolve({ state: "timeout" }), timeoutMs); })
|
|
57
|
+
]);
|
|
58
|
+
} finally {
|
|
59
|
+
if (timer) clearTimeout(timer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class SessionOperationWatchdog {
|
|
64
|
+
readonly policy: SessionOperationDeadlinePolicy;
|
|
65
|
+
#idleTimer: ReturnType<typeof setTimeout> | null = null;
|
|
66
|
+
#maximumTimer: ReturnType<typeof setTimeout> | null = null;
|
|
67
|
+
#expire: ((reason: SessionOperationDeadlineReason) => void) | null = null;
|
|
68
|
+
#closed = false;
|
|
69
|
+
#termination: Promise<unknown> | null = null;
|
|
70
|
+
|
|
71
|
+
constructor(policy: SessionOperationDeadlinePolicy) { this.policy = policy; }
|
|
72
|
+
get terminating(): boolean { return this.#termination !== null; }
|
|
73
|
+
|
|
74
|
+
start(expire: (reason: SessionOperationDeadlineReason) => void): void {
|
|
75
|
+
if (this.#expire || this.#closed) throw new Error("session-operation-watchdog-state-invalid");
|
|
76
|
+
this.#expire = expire;
|
|
77
|
+
this.#armIdle();
|
|
78
|
+
this.#maximumTimer = setTimeout(() => this.#deadline("operation-deadline-exceeded"), this.policy.maximumDurationMs);
|
|
79
|
+
// An accepted operation owns live runtime work until it settles. Keep both
|
|
80
|
+
// deadline timers referenced so a headless Gateway cannot let Node exit
|
|
81
|
+
// while the provider promise is pending and silently skip recovery.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
progress(): void {
|
|
85
|
+
if (this.#closed || !this.#expire) return;
|
|
86
|
+
this.#armIdle();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
close(): void {
|
|
90
|
+
if (this.#closed) return;
|
|
91
|
+
this.#closed = true;
|
|
92
|
+
if (this.#idleTimer) clearTimeout(this.#idleTimer);
|
|
93
|
+
if (this.#maximumTimer) clearTimeout(this.#maximumTimer);
|
|
94
|
+
this.#idleTimer = null; this.#maximumTimer = null; this.#expire = null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
terminate<T>(run: () => Promise<T>): Promise<T> {
|
|
98
|
+
if (!this.#termination) { this.close(); this.#termination = Promise.resolve().then(run); }
|
|
99
|
+
return this.#termination as Promise<T>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#armIdle(): void {
|
|
103
|
+
if (this.#idleTimer) clearTimeout(this.#idleTimer);
|
|
104
|
+
this.#idleTimer = setTimeout(() => this.#deadline("operation-inactivity-timeout"), this.policy.inactivityTimeoutMs);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
#deadline(reason: SessionOperationDeadlineReason): void {
|
|
108
|
+
if (this.#closed) return;
|
|
109
|
+
const expire = this.#expire; this.close(); expire?.(reason);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function armSessionOperationWatchdog(options: {
|
|
114
|
+
watchdog: SessionOperationWatchdog;
|
|
115
|
+
subscribe(listener: (event: unknown) => void): unknown;
|
|
116
|
+
observe(event: unknown): void;
|
|
117
|
+
expire(reason: SessionOperationDeadlineReason): void;
|
|
118
|
+
}): () => void {
|
|
119
|
+
const unsubscribe = options.subscribe((event) => { options.watchdog.progress(); options.observe(event); });
|
|
120
|
+
if (typeof unsubscribe !== "function") throw new Error("session-operation-subscribe-invalid");
|
|
121
|
+
try { options.watchdog.start(options.expire); }
|
|
122
|
+
catch (error) { try { unsubscribe(); } catch { /* authority is quarantined by the caller */ } throw error; }
|
|
123
|
+
return () => { unsubscribe(); };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function terminateWatchedSessionOperation(options: {
|
|
127
|
+
watchdog: SessionOperationWatchdog;
|
|
128
|
+
settlement: "aborted" | "error";
|
|
129
|
+
reasonCode: string;
|
|
130
|
+
forcedReasonCode: string;
|
|
131
|
+
stream: { markAborted(reasonCode: string): void; markError(reasonCode: string): void;
|
|
132
|
+
forceLifecycleTermination(reasonCode: string): void };
|
|
133
|
+
completion(): Promise<void> | null;
|
|
134
|
+
settledCleanly(): boolean;
|
|
135
|
+
cancelApproval(reasonCode: string): void;
|
|
136
|
+
abortHost(): unknown;
|
|
137
|
+
clearQueue?(): void;
|
|
138
|
+
}): Promise<SessionOperationTerminationResult> {
|
|
139
|
+
return await options.watchdog.terminate(async () => {
|
|
140
|
+
const mark = (reasonCode: string) => options.settlement === "aborted"
|
|
141
|
+
? options.stream.markAborted(reasonCode) : options.stream.markError(reasonCode);
|
|
142
|
+
let cleanupFailed = false;
|
|
143
|
+
try { options.cancelApproval(options.reasonCode); } catch { cleanupFailed = true; }
|
|
144
|
+
try { options.clearQueue?.(); } catch { cleanupFailed = true; }
|
|
145
|
+
const terminalReason = cleanupFailed ? TERMINATION_CLEANUP_FAILED : options.reasonCode;
|
|
146
|
+
mark(terminalReason);
|
|
147
|
+
void Promise.resolve().then(options.abortHost).catch(() => undefined);
|
|
148
|
+
const completion = options.completion();
|
|
149
|
+
const completed = completion ? await boundedResult(completion, options.watchdog.policy.terminationTimeoutMs)
|
|
150
|
+
: { state: "timeout" as const };
|
|
151
|
+
if (!cleanupFailed && completed.state === "settled" && options.settledCleanly()) return { state: "settled" as const };
|
|
152
|
+
const quarantineReason = cleanupFailed ? TERMINATION_CLEANUP_FAILED : options.forcedReasonCode;
|
|
153
|
+
mark(quarantineReason); options.stream.forceLifecycleTermination(quarantineReason);
|
|
154
|
+
const forcedCompletion = options.completion();
|
|
155
|
+
if (forcedCompletion) await boundedResult(forcedCompletion, options.watchdog.policy.projectionTimeoutMs);
|
|
156
|
+
return { state: "quarantine" as const, reasonCode: quarantineReason };
|
|
157
|
+
});
|
|
158
|
+
}
|