@piagent/platform 1.5.4 → 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 +136 -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 +9 -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 +3 -0
- package/packages/piagent-core/extensions/javascript-regex-evidence.js +32 -0
- package/packages/piagent-core/extensions/piagent-guard.ts +250 -43
- package/packages/piagent-core/extensions/policy-core.js +1 -1
- 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 +3 -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 +9 -3
- package/packages/piagent-core/runtime/inspection/activity-recovery.ts +104 -0
- package/packages/piagent-core/runtime/inspection/approval-broker.ts +29 -5
- 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 -120
- 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/piagent-dashboard.mjs +13 -1
- 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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import type { PiSessionInfo } from "./session-catalog.ts";
|
|
4
|
+
import { preferAuthoritativePiagentGuard } from "./extension-authority.ts";
|
|
5
|
+
import { rpcUiContext } from "./rpc-ui-context.ts";
|
|
6
|
+
|
|
7
|
+
export type RuntimeHandle = { dispose(): Promise<void>; session?: any };
|
|
8
|
+
export type RuntimeFactory = (info: PiSessionInfo, runtimeInstanceRef: string,
|
|
9
|
+
sessionManager?: any) => Promise<RuntimeHandle>;
|
|
10
|
+
|
|
11
|
+
export function createProductionRuntimeFactory(options: {
|
|
12
|
+
host: any;
|
|
13
|
+
agentDir: string;
|
|
14
|
+
packageRoot: string;
|
|
15
|
+
modelRuntime?: any;
|
|
16
|
+
}): RuntimeFactory {
|
|
17
|
+
return async (info, _runtimeInstanceRef, sessionManager) => {
|
|
18
|
+
const guard = path.join(options.packageRoot, "packages", "piagent-core", "extensions", "piagent-guard.ts");
|
|
19
|
+
const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }: any) => {
|
|
20
|
+
const services = await options.host.createAgentSessionServices({
|
|
21
|
+
cwd, agentDir, modelRuntime: options.modelRuntime,
|
|
22
|
+
resourceLoaderOptions: {
|
|
23
|
+
additionalExtensionPaths: [guard],
|
|
24
|
+
extensionsOverride: preferAuthoritativePiagentGuard(guard)
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
const extensionErrors = services.resourceLoader.getExtensions().errors;
|
|
28
|
+
if (extensionErrors.length) throw new Error("session-runtime-extension-load-failed");
|
|
29
|
+
const created = await options.host.createAgentSessionFromServices({ services, sessionManager, sessionStartEvent });
|
|
30
|
+
return { ...created, services, diagnostics: services.diagnostics };
|
|
31
|
+
};
|
|
32
|
+
const manager = sessionManager ?? options.host.SessionManager.open(info.path);
|
|
33
|
+
const runtime = await options.host.createAgentSessionRuntime(createRuntime, {
|
|
34
|
+
cwd: info.cwd,
|
|
35
|
+
agentDir: options.agentDir,
|
|
36
|
+
sessionManager: manager,
|
|
37
|
+
sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile: info.path }
|
|
38
|
+
});
|
|
39
|
+
const bind = async (session: any) => session.bindExtensions({ mode: "rpc", uiContext: rpcUiContext() });
|
|
40
|
+
runtime.setRebindSession(bind);
|
|
41
|
+
try { await bind(runtime.session); }
|
|
42
|
+
catch (error) { await runtime.dispose().catch(() => undefined); throw error; }
|
|
43
|
+
return runtime;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
import { webUiModelRef } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
|
|
5
|
-
import { webUiTaskRevision } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
|
|
2
|
+
import { webUiModelRef, webUiTaskRevision } from "../../piagent-core/runtime/inspection/webui-snapshot.ts";
|
|
6
3
|
import { activeSessionTask } from "../../piagent-core/extensions/task-state.js";
|
|
7
4
|
import { inspectTaskControlState } from "../../piagent-core/runtime/inspection/task-control-journal.ts";
|
|
8
5
|
import { piApprovalBroker, type ApprovalAuthority, type ApprovalBrokerEvent } from "../../piagent-core/runtime/inspection/approval-broker.ts";
|
|
@@ -11,52 +8,24 @@ import { isUserConversationSession, projectRefForCwd, sessionRefForPath } from "
|
|
|
11
8
|
import { SessionLeaseStore, type SessionLeaseSnapshot } from "./session-lease-store.ts";
|
|
12
9
|
import { GatewayEventStore } from "./gateway-events.ts";
|
|
13
10
|
import { GatewaySessionStream } from "./gateway-session-stream.ts";
|
|
14
|
-
import { preferAuthoritativePiagentGuard } from "./extension-authority.ts";
|
|
15
11
|
import { executePermissionCommand, executeRuntimeCommand } from "./runtime-session-controls.ts";
|
|
16
|
-
import {
|
|
12
|
+
import { configureSessionOptions, effectiveModelRef, effectiveThinkingLevel,
|
|
13
|
+
type EffectiveSessionOptions } from "./session-effective-options.ts";
|
|
17
14
|
import { waitForOperationStart } from "./session-operation-start.ts";
|
|
18
|
-
|
|
15
|
+
import { armSessionOperationWatchdog, bestEffortUnsubscribe, boundedResult, sessionOperationDeadlinePolicy, SessionOperationWatchdog, terminateWatchedSessionOperation,
|
|
16
|
+
type SessionOperationDeadlinePolicy, type SessionOperationWatchdogOptions } from "./session-operation-watchdog.ts";
|
|
17
|
+
import { createProductionRuntimeFactory, type RuntimeFactory, type RuntimeHandle } from "./session-runtime-factory.ts";
|
|
19
18
|
const MAX_WARM_RUNTIMES = 10;
|
|
20
|
-
|
|
21
|
-
type RuntimeHandle = { dispose(): Promise<void>; session?: any };
|
|
22
|
-
type RuntimeFactory = (info: PiSessionInfo, runtimeInstanceRef: string, sessionManager?: any) => Promise<RuntimeHandle>;
|
|
23
19
|
type ActiveRuntime = { runtime: RuntimeHandle; lease: SessionLeaseSnapshot; info: PiSessionInfo; operationRef: string | null;
|
|
24
|
-
unsubscribe: (() => void) | null; completion: Promise<void> | null; settling: boolean; approvalWaiting: boolean;
|
|
25
|
-
unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null; sessionManager: any | null
|
|
20
|
+
stream: GatewaySessionStream | null; unsubscribe: (() => void) | null; completion: Promise<void> | null; settling: boolean; approvalWaiting: boolean;
|
|
21
|
+
unbindApproval: (() => void) | null; unsubscribeApproval: (() => void) | null; sessionManager: any | null;
|
|
22
|
+
watchdog: SessionOperationWatchdog | null; lastSessionRevision: string | null };
|
|
26
23
|
type Projection = { sessionRevision: string; liveState: "offline" | "idle" | "running" | "paused" | "waiting-approval" | "uncertain" };
|
|
27
24
|
type RuntimeCommandResult = Awaited<ReturnType<typeof executeRuntimeCommand>>;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const createRuntime = async ({ cwd, agentDir, sessionManager, sessionStartEvent }: any) => {
|
|
33
|
-
const services = await options.host.createAgentSessionServices({
|
|
34
|
-
cwd, agentDir, modelRuntime: options.modelRuntime,
|
|
35
|
-
resourceLoaderOptions: {
|
|
36
|
-
additionalExtensionPaths: [guard],
|
|
37
|
-
extensionsOverride: preferAuthoritativePiagentGuard(guard)
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
const extensionErrors = services.resourceLoader.getExtensions().errors;
|
|
41
|
-
if (extensionErrors.length) throw new Error("session-runtime-extension-load-failed");
|
|
42
|
-
const created = await options.host.createAgentSessionFromServices({ services, sessionManager, sessionStartEvent });
|
|
43
|
-
return { ...created, services, diagnostics: services.diagnostics };
|
|
44
|
-
};
|
|
45
|
-
const manager = sessionManager ?? options.host.SessionManager.open(info.path);
|
|
46
|
-
const runtime = await options.host.createAgentSessionRuntime(createRuntime, {
|
|
47
|
-
cwd: info.cwd,
|
|
48
|
-
agentDir: options.agentDir,
|
|
49
|
-
sessionManager: manager,
|
|
50
|
-
sessionStartEvent: { type: "session_start", reason: "resume", previousSessionFile: info.path }
|
|
51
|
-
});
|
|
52
|
-
const bind = async (session: any) => session.bindExtensions({ mode: "rpc", uiContext: rpcUiContext() });
|
|
53
|
-
runtime.setRebindSession(bind);
|
|
54
|
-
try { await bind(runtime.session); }
|
|
55
|
-
catch (error) { await runtime.dispose().catch(() => undefined); throw error; }
|
|
56
|
-
return runtime;
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
25
|
+
export type { EffectiveSessionOptions } from "./session-effective-options.ts";
|
|
26
|
+
export type SessionCreateResult = { sessionRef: string; effectiveOptions: EffectiveSessionOptions };
|
|
27
|
+
export type CurrentOperationProjection = { operationRef: string; state: "running" | "waiting-approval" | "settling";
|
|
28
|
+
abortable: boolean };
|
|
60
29
|
export class SessionRuntimeSupervisor {
|
|
61
30
|
readonly #gatewayInstanceRef: string;
|
|
62
31
|
readonly #key: Buffer;
|
|
@@ -65,13 +34,13 @@ export class SessionRuntimeSupervisor {
|
|
|
65
34
|
readonly #runtimeFactory: RuntimeFactory;
|
|
66
35
|
readonly #events: GatewayEventStore;
|
|
67
36
|
readonly #host: any | null;
|
|
37
|
+
readonly #operationDeadlinePolicy: SessionOperationDeadlinePolicy;
|
|
68
38
|
readonly #resolveProject: ((projectRef: string) => string | null) | null;
|
|
69
39
|
readonly #active = new Map<string, ActiveRuntime>();
|
|
70
40
|
readonly #opening = new Map<string, Promise<SessionLeaseSnapshot>>();
|
|
71
41
|
readonly #created = new Map<string, PiSessionInfo>();
|
|
72
42
|
#closed = false;
|
|
73
43
|
#readProjection: ((sessionRef: string) => Promise<Projection>) | null = null;
|
|
74
|
-
|
|
75
44
|
constructor(options: {
|
|
76
45
|
gatewayInstanceRef: string;
|
|
77
46
|
key: Buffer;
|
|
@@ -83,6 +52,7 @@ export class SessionRuntimeSupervisor {
|
|
|
83
52
|
packageRoot?: string;
|
|
84
53
|
modelRuntime?: any;
|
|
85
54
|
events?: GatewayEventStore;
|
|
55
|
+
operationWatchdog?: SessionOperationWatchdogOptions;
|
|
86
56
|
resolveProject?(projectRef: string): string | null;
|
|
87
57
|
}) {
|
|
88
58
|
this.#gatewayInstanceRef = options.gatewayInstanceRef;
|
|
@@ -91,19 +61,17 @@ export class SessionRuntimeSupervisor {
|
|
|
91
61
|
this.#listSessions = options.listSessions;
|
|
92
62
|
this.#events = options.events ?? new GatewayEventStore();
|
|
93
63
|
this.#host = options.host ?? null;
|
|
64
|
+
this.#operationDeadlinePolicy = sessionOperationDeadlinePolicy(options.operationWatchdog);
|
|
94
65
|
this.#resolveProject = options.resolveProject ?? null;
|
|
95
66
|
if (options.runtimeFactory) this.#runtimeFactory = options.runtimeFactory;
|
|
96
67
|
else {
|
|
97
68
|
if (!options.host || !options.agentDir || !options.packageRoot) throw new Error("session-runtime-factory-unavailable");
|
|
98
|
-
this.#runtimeFactory =
|
|
69
|
+
this.#runtimeFactory = createProductionRuntimeFactory({ host: options.host, agentDir: options.agentDir,
|
|
99
70
|
packageRoot: options.packageRoot, modelRuntime: options.modelRuntime });
|
|
100
71
|
}
|
|
101
72
|
}
|
|
102
|
-
|
|
103
73
|
get activeCount(): number { return this.#active.size; }
|
|
104
|
-
|
|
105
74
|
setProjectionReader(reader: (sessionRef: string) => Promise<Projection>): void { this.#readProjection = reader; }
|
|
106
|
-
|
|
107
75
|
async listSessions(): Promise<PiSessionInfo[]> {
|
|
108
76
|
const persisted = (await this.#listSessions()).filter(isUserConversationSession);
|
|
109
77
|
const paths = new Set(persisted.map((info) => info.path));
|
|
@@ -112,8 +80,11 @@ export class SessionRuntimeSupervisor {
|
|
|
112
80
|
}
|
|
113
81
|
return [...persisted, ...[...this.#created.values()].filter((info) => !paths.has(info.path))];
|
|
114
82
|
}
|
|
115
|
-
|
|
116
83
|
async create(projectRef: string, placeRef: string, modelRef: string | null, thinkingLevel: string): Promise<string> {
|
|
84
|
+
return (await this.createWithReadback(projectRef, placeRef, modelRef, thinkingLevel)).sessionRef;
|
|
85
|
+
}
|
|
86
|
+
async createWithReadback(projectRef: string, placeRef: string, modelRef: string | null,
|
|
87
|
+
thinkingLevel: string): Promise<SessionCreateResult> {
|
|
117
88
|
if (this.#closed || !this.#host) throw new Error("session-create-unavailable");
|
|
118
89
|
if (placeRef !== projectRef) throw new Error("session-place-mismatch");
|
|
119
90
|
const sessions = (await this.#listSessions()).filter(isUserConversationSession);
|
|
@@ -128,22 +99,19 @@ export class SessionRuntimeSupervisor {
|
|
|
128
99
|
const created: PiSessionInfo = { path: sessionFile, id: manager.getSessionId(), cwd: candidates[0], created: createdAt,
|
|
129
100
|
modified: createdAt, messageCount: 0, firstMessage: "(no messages)", allMessagesText: "" };
|
|
130
101
|
const sessionRef = sessionRefForPath(this.#key, sessionFile);
|
|
131
|
-
|
|
132
|
-
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
133
|
-
if (!session) throw new Error("session-runtime-unavailable");
|
|
134
|
-
if (modelRef) {
|
|
135
|
-
const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
|
|
136
|
-
const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? "")) === modelRef);
|
|
137
|
-
if (!model) throw new Error("session-model-unavailable");
|
|
138
|
-
await session.setModel(model);
|
|
139
|
-
}
|
|
140
|
-
session.setThinkingLevel(thinkingLevel);
|
|
102
|
+
// Retain identity before fallible runtime work so create receipts never point at an orphan.
|
|
141
103
|
this.#created.set(sessionRef, created);
|
|
142
|
-
|
|
104
|
+
try { await this.#activate(sessionRef, created, manager); }
|
|
105
|
+
catch {
|
|
106
|
+
return { sessionRef, effectiveOptions: { state: "unknown", modelRef: null, thinkingLevel: null,
|
|
107
|
+
reasonCode: "session-runtime-open-failed" } };
|
|
108
|
+
}
|
|
109
|
+
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
110
|
+
if (!session) return { sessionRef, effectiveOptions: { state: "unknown", modelRef: null, thinkingLevel: null,
|
|
111
|
+
reasonCode: "session-runtime-unavailable" } };
|
|
112
|
+
return { sessionRef, effectiveOptions: await configureSessionOptions(session, modelRef, thinkingLevel) };
|
|
143
113
|
}
|
|
144
|
-
|
|
145
114
|
liveSessionManager(sessionRef: string): any | null { return this.#active.get(sessionRef)?.sessionManager ?? null; }
|
|
146
|
-
|
|
147
115
|
async acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
|
|
148
116
|
if (this.#closed) throw new Error("session-runtime-supervisor-closed");
|
|
149
117
|
const active = this.#active.get(sessionRef);
|
|
@@ -154,7 +122,6 @@ export class SessionRuntimeSupervisor {
|
|
|
154
122
|
this.#opening.set(sessionRef, opening);
|
|
155
123
|
return await opening;
|
|
156
124
|
}
|
|
157
|
-
|
|
158
125
|
async #acquire(sessionRef: string): Promise<SessionLeaseSnapshot> {
|
|
159
126
|
if (this.#active.size + this.#opening.size > MAX_WARM_RUNTIMES) throw new Error("session-runtime-capacity");
|
|
160
127
|
const found = (await this.#listSessions()).filter(isUserConversationSession)
|
|
@@ -162,7 +129,6 @@ export class SessionRuntimeSupervisor {
|
|
|
162
129
|
if (found.length !== 1) throw new Error(found.length ? "session-ref-ambiguous" : "session-not-found");
|
|
163
130
|
return await this.#activate(sessionRef, found[0]!);
|
|
164
131
|
}
|
|
165
|
-
|
|
166
132
|
async #activate(sessionRef: string, info: PiSessionInfo, sessionManager?: any): Promise<SessionLeaseSnapshot> {
|
|
167
133
|
if (this.#active.has(sessionRef)) throw new Error("session-owner-conflict");
|
|
168
134
|
const runtimeInstanceRef = `runtime_${randomBytes(24).toString("base64url")}`;
|
|
@@ -179,9 +145,9 @@ export class SessionRuntimeSupervisor {
|
|
|
179
145
|
await runtime.dispose().catch(() => undefined);
|
|
180
146
|
throw new Error("session-owner-continuity-lost");
|
|
181
147
|
}
|
|
182
|
-
const active: ActiveRuntime = { runtime, lease: current, info, operationRef: null, unsubscribe: null, completion: null,
|
|
148
|
+
const active: ActiveRuntime = { runtime, lease: current, info, operationRef: null, stream: null, unsubscribe: null, completion: null,
|
|
183
149
|
settling: false, approvalWaiting: false, unbindApproval: null, unsubscribeApproval: null,
|
|
184
|
-
sessionManager: sessionManager ?? runtime.session?.sessionManager ?? null };
|
|
150
|
+
sessionManager: sessionManager ?? runtime.session?.sessionManager ?? null, watchdog: null, lastSessionRevision: null };
|
|
185
151
|
this.#active.set(sessionRef, active);
|
|
186
152
|
this.#bindApproval(sessionRef, active);
|
|
187
153
|
return current;
|
|
@@ -191,7 +157,6 @@ export class SessionRuntimeSupervisor {
|
|
|
191
157
|
throw error;
|
|
192
158
|
}
|
|
193
159
|
}
|
|
194
|
-
|
|
195
160
|
async release(sessionRef: string): Promise<SessionLeaseSnapshot> {
|
|
196
161
|
const active = this.#active.get(sessionRef);
|
|
197
162
|
if (!active) {
|
|
@@ -203,7 +168,7 @@ export class SessionRuntimeSupervisor {
|
|
|
203
168
|
if (active.settling && active.completion) await active.completion;
|
|
204
169
|
if (active.operationRef || active.settling) throw new Error("session-runtime-busy");
|
|
205
170
|
this.#active.delete(sessionRef);
|
|
206
|
-
active.unsubscribeApproval
|
|
171
|
+
bestEffortUnsubscribe(active.unsubscribeApproval); bestEffortUnsubscribe(active.unbindApproval);
|
|
207
172
|
try { await active.runtime.dispose(); }
|
|
208
173
|
catch (error) {
|
|
209
174
|
try { this.#leases.requireRecovery(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef,
|
|
@@ -215,7 +180,28 @@ export class SessionRuntimeSupervisor {
|
|
|
215
180
|
if (transient && !(await this.#listSessions()).some((info) => info.path === transient.path)) this.#created.delete(sessionRef);
|
|
216
181
|
return released;
|
|
217
182
|
}
|
|
218
|
-
|
|
183
|
+
async restart(sessionRef: string): Promise<SessionLeaseSnapshot> {
|
|
184
|
+
const active = this.#active.get(sessionRef);
|
|
185
|
+
if (!active) return await this.acquire(sessionRef);
|
|
186
|
+
if (active.operationRef || active.settling || !active.runtime.session?.isIdle) throw new Error("session-runtime-busy");
|
|
187
|
+
const info = active.info, manager = active.sessionManager; await this.release(sessionRef);
|
|
188
|
+
return await this.#activate(sessionRef, info, manager ?? undefined);
|
|
189
|
+
}
|
|
190
|
+
currentOperation(sessionRef: string): CurrentOperationProjection | null {
|
|
191
|
+
const active = this.#active.get(sessionRef);
|
|
192
|
+
if (!active?.operationRef) return null;
|
|
193
|
+
return { operationRef: active.operationRef,
|
|
194
|
+
state: active.settling ? "settling" : active.approvalWaiting ? "waiting-approval" : "running",
|
|
195
|
+
abortable: !active.settling };
|
|
196
|
+
}
|
|
197
|
+
currentOperations(): Array<CurrentOperationProjection & { sessionRef: string }> {
|
|
198
|
+
const result: Array<CurrentOperationProjection & { sessionRef: string }> = [];
|
|
199
|
+
for (const sessionRef of this.#active.keys()) {
|
|
200
|
+
const operation = this.currentOperation(sessionRef);
|
|
201
|
+
if (operation) result.push({ sessionRef, ...operation });
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
219
205
|
ownership(sessionRef: string): SessionOwnerProjection {
|
|
220
206
|
const lease = this.#leases.inspect(sessionRef);
|
|
221
207
|
const active = this.#active.get(sessionRef);
|
|
@@ -225,7 +211,7 @@ export class SessionRuntimeSupervisor {
|
|
|
225
211
|
reasonCode: null
|
|
226
212
|
};
|
|
227
213
|
if (lease.state === "gateway-owned" && active && active.lease.ownerEpoch === lease.ownerEpoch
|
|
228
|
-
&& lease.gatewayInstanceRef === this.#gatewayInstanceRef) return {
|
|
214
|
+
&& lease.gatewayInstanceRef === this.#gatewayInstanceRef && active.lease.runtimeInstanceRef === lease.runtimeInstanceRef) return {
|
|
229
215
|
state: "gateway-owned", liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : "idle",
|
|
230
216
|
composerAvailable: true, needsAttention: active.approvalWaiting,
|
|
231
217
|
owner: { kind: "gateway", ownerEpoch: lease.ownerEpoch!, gatewayInstanceRef: lease.gatewayInstanceRef!,
|
|
@@ -250,10 +236,6 @@ export class SessionRuntimeSupervisor {
|
|
|
250
236
|
reasonCode: lease.reasonCode ?? "session-owner-continuity-unknown"
|
|
251
237
|
};
|
|
252
238
|
}
|
|
253
|
-
|
|
254
|
-
// `message` arrives already carrying any attached document text, and `images`
|
|
255
|
-
// already split out: the command controller claims staged refs before it calls
|
|
256
|
-
// here, so this layer only forwards what the host prompt takes.
|
|
257
239
|
async send(sessionRef: string, payload: { delivery: "new-operation" | "follow-up" | "steer"; message: string;
|
|
258
240
|
expectedOperationRef: string | null; images?: unknown[] }, sessionRevision: string):
|
|
259
241
|
Promise<{ resultCode: "started" | "queued" | "steered"; operationRef: string }> {
|
|
@@ -261,7 +243,7 @@ export class SessionRuntimeSupervisor {
|
|
|
261
243
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
262
244
|
if (!active || !session) throw new Error("session-runtime-unavailable");
|
|
263
245
|
if (payload.delivery !== "new-operation") {
|
|
264
|
-
if (!active.operationRef || active.operationRef !== payload.expectedOperationRef || !session.isStreaming) throw new Error("session-operation-conflict");
|
|
246
|
+
if (!active.operationRef || active.operationRef !== payload.expectedOperationRef || active.settling || !session.isStreaming) throw new Error("session-operation-conflict");
|
|
265
247
|
const images = payload.images?.length ? payload.images : undefined;
|
|
266
248
|
if (payload.delivery === "follow-up") { await session.followUp(payload.message, images); return { resultCode: "queued", operationRef: active.operationRef }; }
|
|
267
249
|
await session.steer(payload.message, images); return { resultCode: "steered", operationRef: active.operationRef };
|
|
@@ -269,8 +251,14 @@ export class SessionRuntimeSupervisor {
|
|
|
269
251
|
if (payload.expectedOperationRef !== null || active.operationRef || active.settling || !session.isIdle) throw new Error("session-operation-conflict");
|
|
270
252
|
const operationRef = `operation_${randomBytes(24).toString("base64url")}`;
|
|
271
253
|
const stream = new GatewaySessionStream({ sessionRef, operationRef, events: this.#events });
|
|
272
|
-
|
|
273
|
-
active.
|
|
254
|
+
const watchdog = new SessionOperationWatchdog(this.#operationDeadlinePolicy);
|
|
255
|
+
active.operationRef = operationRef; active.stream = stream; active.watchdog = watchdog; active.lastSessionRevision = sessionRevision;
|
|
256
|
+
try {
|
|
257
|
+
active.unsubscribe = armSessionOperationWatchdog({ watchdog, subscribe: (listener) => session.subscribe(listener),
|
|
258
|
+
observe: (event) => stream.observe(event), expire: (reasonCode) => { void this.#terminateOperation(sessionRef,
|
|
259
|
+
operationRef, "error", reasonCode, reasonCode, true).catch(() => undefined); } });
|
|
260
|
+
} catch (error) { stream.markError("session-operation-start-failed");
|
|
261
|
+
await this.#quarantineRuntime(sessionRef, operationRef, active, stream, "session-operation-start-failed"); throw error; }
|
|
274
262
|
this.#events.publish("runtime.changed", { sessionRef, sessionRevision, liveState: "running", operationRef, reasonCode: null });
|
|
275
263
|
const created = this.#created.get(sessionRef);
|
|
276
264
|
if (created && created.firstMessage === "(no messages)") {
|
|
@@ -278,31 +266,51 @@ export class SessionRuntimeSupervisor {
|
|
|
278
266
|
}
|
|
279
267
|
let prompt: Promise<void>;
|
|
280
268
|
try { prompt = session.prompt(payload.message, payload.images?.length ? { images: payload.images } : undefined); }
|
|
281
|
-
catch (error) { await this.#finishOperation(sessionRef, operationRef, stream); throw error; }
|
|
269
|
+
catch (error) { stream.markError(); await this.#finishOperation(sessionRef, operationRef, stream); throw error; }
|
|
282
270
|
const started = waitForOperationStart(stream, prompt);
|
|
283
|
-
//
|
|
284
|
-
// command resolves before its nested agent turn starts. In both cases the
|
|
285
|
-
// canonical completion boundary is `agent_settled`, not the outer promise.
|
|
271
|
+
// `agent_settled` is canonical even when a workflow's outer prompt resolves first.
|
|
286
272
|
const lifecycle = started.then(async (mode) => {
|
|
287
273
|
if (mode === "deferred") await stream.settled();
|
|
288
274
|
else await Promise.race([stream.settled(), prompt]);
|
|
289
275
|
});
|
|
290
276
|
active.completion = lifecycle.then(() => this.#finishOperation(sessionRef, operationRef, stream),
|
|
291
|
-
() => this.#finishOperation(sessionRef, operationRef, stream));
|
|
277
|
+
() => { stream.markError(); return this.#finishOperation(sessionRef, operationRef, stream); });
|
|
292
278
|
try { await started; }
|
|
293
279
|
catch (error) { await active.completion; throw error; }
|
|
294
280
|
return { resultCode: "started", operationRef };
|
|
295
281
|
}
|
|
296
|
-
|
|
297
282
|
async abort(sessionRef: string, operationRef: string, clearQueued: boolean): Promise<void> {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
283
|
+
await this.#terminateOperation(sessionRef, operationRef, "aborted", "operation-aborted", "operation-abort-timeout", clearQueued);
|
|
284
|
+
}
|
|
285
|
+
async #terminateOperation(sessionRef: string, operationRef: string, settlement: "aborted" | "error",
|
|
286
|
+
reasonCode: string, forcedReasonCode: string, clearQueued: boolean): Promise<void> {
|
|
287
|
+
const active = this.#active.get(sessionRef), session = active?.runtime.session, stream = active?.stream, watchdog = active?.watchdog;
|
|
288
|
+
if (!active || !session || !stream || !watchdog || active.operationRef !== operationRef) throw new Error("session-operation-conflict");
|
|
289
|
+
if (active.settling && !watchdog.terminating) throw new Error("session-operation-conflict");
|
|
290
|
+
active.settling = true; active.approvalWaiting = false;
|
|
291
|
+
const result = await terminateWatchedSessionOperation({ watchdog, settlement, reasonCode, forcedReasonCode, stream,
|
|
292
|
+
completion: () => active.completion,
|
|
293
|
+
settledCleanly: () => this.#active.get(sessionRef) === active && active.operationRef !== operationRef
|
|
294
|
+
&& session.isIdle === true && session.isStreaming !== true,
|
|
295
|
+
cancelApproval: (reason) => { piApprovalBroker.cancelForOperation(active.info.cwd, active.info.id, operationRef, reason); },
|
|
296
|
+
abortHost: () => session.abort(), clearQueue: clearQueued ? () => session.clearQueue() : undefined
|
|
297
|
+
});
|
|
298
|
+
if (result.state === "quarantine") await this.#quarantineRuntime(sessionRef, operationRef, active, stream, result.reasonCode);
|
|
299
|
+
else if (this.#active.get(sessionRef) === active) { active.stream = null; active.watchdog = null; active.settling = false; }
|
|
300
|
+
}
|
|
301
|
+
async #quarantineRuntime(sessionRef: string, operationRef: string, active: ActiveRuntime,
|
|
302
|
+
stream: GatewaySessionStream, reasonCode: string): Promise<void> {
|
|
303
|
+
if (this.#active.get(sessionRef) !== active) return;
|
|
304
|
+
bestEffortUnsubscribe(active.unsubscribe); active.unsubscribe = null; active.watchdog?.close(); active.watchdog = null;
|
|
305
|
+
if (active.operationRef === operationRef) { active.operationRef = null; stream.complete(null); }
|
|
306
|
+
active.stream = null; active.completion = null; active.settling = false; active.approvalWaiting = false;
|
|
307
|
+
this.#active.delete(sessionRef); bestEffortUnsubscribe(active.unsubscribeApproval); bestEffortUnsubscribe(active.unbindApproval);
|
|
308
|
+
try { this.#leases.requireRecovery(sessionRef, active.lease.ownerEpoch!, this.#gatewayInstanceRef,
|
|
309
|
+
active.lease.runtimeInstanceRef!, reasonCode); } catch { /* continuity remains fail closed */ }
|
|
310
|
+
if (active.lastSessionRevision) this.#events.publish("runtime.changed", { sessionRef,
|
|
311
|
+
sessionRevision: active.lastSessionRevision, liveState: "uncertain", operationRef: null, reasonCode });
|
|
312
|
+
await boundedResult(Promise.resolve().then(() => active.runtime.dispose()), this.#operationDeadlinePolicy.terminationTimeoutMs);
|
|
304
313
|
}
|
|
305
|
-
|
|
306
314
|
#bindApproval(sessionRef: string, active: ActiveRuntime): void {
|
|
307
315
|
const authority = (): ApprovalAuthority => {
|
|
308
316
|
if (!active.operationRef || active.lease.state !== "gateway-owned" || !active.lease.revision) return null;
|
|
@@ -327,50 +335,53 @@ export class SessionRuntimeSupervisor {
|
|
|
327
335
|
active.unbindApproval = piApprovalBroker.bind({ cwd: active.info.cwd, rawSessionId: active.info.id,
|
|
328
336
|
runtimeInstanceId: active.lease.runtimeInstanceRef!, authority });
|
|
329
337
|
active.unsubscribeApproval = piApprovalBroker.subscribe(active.info.cwd, active.info.id, (event: ApprovalBrokerEvent) => {
|
|
338
|
+
active.watchdog?.progress();
|
|
330
339
|
const projection = piApprovalBroker.projection(active.info.cwd, active.info.id);
|
|
331
340
|
active.approvalWaiting = projection.summary.state === "waiting";
|
|
332
341
|
void this.#publishApprovalState(sessionRef, active, event);
|
|
333
342
|
});
|
|
334
343
|
}
|
|
335
|
-
|
|
336
344
|
async #publishApprovalState(sessionRef: string, active: ActiveRuntime, event: ApprovalBrokerEvent): Promise<void> {
|
|
337
345
|
try {
|
|
338
346
|
const projection = await this.#readProjection?.(sessionRef);
|
|
339
347
|
if (!projection || this.#active.get(sessionRef) !== active) return;
|
|
348
|
+
active.lastSessionRevision = projection.sessionRevision;
|
|
340
349
|
this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
|
|
341
350
|
liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : projection.liveState,
|
|
342
351
|
operationRef: active.operationRef, reasonCode: `approval-${event.kind}` });
|
|
343
352
|
} catch { /* approval truth remains available through the canonical inspection route */ }
|
|
344
353
|
}
|
|
345
|
-
|
|
346
354
|
async setModel(sessionRef: string, modelRef: string): Promise<"model-changed" | "no-change"> {
|
|
347
355
|
await this.acquire(sessionRef);
|
|
348
356
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
349
357
|
if (!active || !session) throw new Error("session-runtime-unavailable");
|
|
350
358
|
if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
|
|
351
|
-
const current = session
|
|
352
|
-
? webUiModelRef(String(session.model.provider ?? ""), String(session.model.id ?? session.model.modelId ?? "")) : null;
|
|
359
|
+
const current = effectiveModelRef(session);
|
|
353
360
|
if (current === modelRef) return "no-change";
|
|
354
361
|
const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
|
|
355
|
-
const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? "")) === modelRef);
|
|
362
|
+
const model = models.find((value: any) => webUiModelRef(String(value.provider ?? ""), String(value.id ?? value.modelId ?? "")) === modelRef);
|
|
356
363
|
if (!model) throw new Error("session-model-unavailable");
|
|
357
364
|
await session.setModel(model);
|
|
365
|
+
const effective = effectiveModelRef(session);
|
|
366
|
+
if (!effective) throw new Error("session-model-effect-unknown");
|
|
367
|
+
if (effective !== modelRef) throw new Error("session-model-mismatch");
|
|
358
368
|
this.#touchCreated(sessionRef);
|
|
359
369
|
return "model-changed";
|
|
360
370
|
}
|
|
361
|
-
|
|
362
371
|
async setThinking(sessionRef: string, thinkingLevel: string): Promise<"thinking-changed" | "no-change"> {
|
|
363
372
|
await this.acquire(sessionRef);
|
|
364
373
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
365
374
|
if (!active || !session) throw new Error("session-runtime-unavailable");
|
|
366
375
|
if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
|
|
367
|
-
const current =
|
|
376
|
+
const current = effectiveThinkingLevel(session);
|
|
368
377
|
if (current === thinkingLevel) return "no-change";
|
|
369
|
-
session.setThinkingLevel(thinkingLevel);
|
|
378
|
+
await session.setThinkingLevel(thinkingLevel);
|
|
379
|
+
const effective = effectiveThinkingLevel(session);
|
|
380
|
+
if (!effective) throw new Error("session-thinking-effect-unknown");
|
|
381
|
+
if (effective !== thinkingLevel) throw new Error("session-thinking-mismatch");
|
|
370
382
|
this.#touchCreated(sessionRef);
|
|
371
383
|
return "thinking-changed";
|
|
372
384
|
}
|
|
373
|
-
|
|
374
385
|
async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<"permission-changed"> {
|
|
375
386
|
await this.acquire(sessionRef);
|
|
376
387
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
@@ -380,7 +391,6 @@ export class SessionRuntimeSupervisor {
|
|
|
380
391
|
this.#touchCreated(sessionRef);
|
|
381
392
|
return "permission-changed";
|
|
382
393
|
}
|
|
383
|
-
|
|
384
394
|
async runRuntimeCommand(sessionRef: string, command: string): Promise<RuntimeCommandResult> {
|
|
385
395
|
await this.acquire(sessionRef);
|
|
386
396
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
@@ -394,12 +404,10 @@ export class SessionRuntimeSupervisor {
|
|
|
394
404
|
health: { state: "unavailable", reasonCode: "approval-broker-unavailable", message: "Approval broker is unavailable" } } };
|
|
395
405
|
return piApprovalBroker.projection(active.info.cwd, active.info.id);
|
|
396
406
|
}
|
|
397
|
-
|
|
398
407
|
approvalDetail(sessionRef: string, approvalRef: string): unknown | null {
|
|
399
408
|
const active = this.#active.get(sessionRef);
|
|
400
409
|
return active ? piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef) : null;
|
|
401
410
|
}
|
|
402
|
-
|
|
403
411
|
async decideApproval(approvalRef: string, decision: unknown): Promise<unknown> {
|
|
404
412
|
for (const active of this.#active.values()) {
|
|
405
413
|
if (piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef)) {
|
|
@@ -408,7 +416,6 @@ export class SessionRuntimeSupervisor {
|
|
|
408
416
|
}
|
|
409
417
|
throw new Error("approval-not-pending");
|
|
410
418
|
}
|
|
411
|
-
|
|
412
419
|
async rename(sessionRef: string, title: string): Promise<"renamed" | "no-change"> {
|
|
413
420
|
await this.acquire(sessionRef);
|
|
414
421
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
@@ -420,7 +427,6 @@ export class SessionRuntimeSupervisor {
|
|
|
420
427
|
this.#touchCreated(sessionRef);
|
|
421
428
|
return "renamed";
|
|
422
429
|
}
|
|
423
|
-
|
|
424
430
|
async fork(sessionRef: string, entryRef: string | null, title: string | null): Promise<string> {
|
|
425
431
|
if (!this.#host) throw new Error("session-fork-unavailable");
|
|
426
432
|
await this.acquire(sessionRef);
|
|
@@ -447,28 +453,41 @@ export class SessionRuntimeSupervisor {
|
|
|
447
453
|
this.#created.set(forkRef, created);
|
|
448
454
|
return forkRef;
|
|
449
455
|
}
|
|
450
|
-
|
|
451
|
-
#touchCreated(sessionRef: string): void {
|
|
452
|
-
const created = this.#created.get(sessionRef);
|
|
453
|
-
if (created) created.modified = new Date();
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
+
#touchCreated(sessionRef: string): void { const created = this.#created.get(sessionRef); if (created) created.modified = new Date(); }
|
|
456
457
|
async #finishOperation(sessionRef: string, operationRef: string, stream: GatewaySessionStream): Promise<void> {
|
|
457
458
|
const active = this.#active.get(sessionRef);
|
|
458
459
|
if (!active || active.operationRef !== operationRef) return;
|
|
459
|
-
|
|
460
|
+
const restartRequired = stream.runtimeRestartRequired; let projection: Projection | null = null;
|
|
461
|
+
bestEffortUnsubscribe(active.unsubscribe); active.unsubscribe = null; active.watchdog?.close(); active.settling = true;
|
|
460
462
|
try {
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
463
|
+
if (this.#readProjection) {
|
|
464
|
+
const read = await boundedResult(this.#readProjection(sessionRef), this.#operationDeadlinePolicy.projectionTimeoutMs);
|
|
465
|
+
if (read.state === "settled") projection = read.value;
|
|
466
|
+
else if (read.state === "timeout") stream.markError("session-projection-timeout");
|
|
467
|
+
}
|
|
466
468
|
} catch { /* Canonical refresh failure cannot be replaced by an invented revision. */ }
|
|
467
|
-
|
|
468
|
-
|
|
469
|
+
if (this.#active.get(sessionRef) !== active || active.operationRef !== operationRef) return;
|
|
470
|
+
if (this.ownership(sessionRef).state !== "gateway-owned") { stream.markError("session-owner-continuity-lost"); await this.#quarantineRuntime(sessionRef, operationRef, active, stream, "session-owner-continuity-lost"); return; }
|
|
471
|
+
active.operationRef = null;
|
|
472
|
+
if (projection) active.lastSessionRevision = projection.sessionRevision;
|
|
473
|
+
stream.complete(projection?.sessionRevision ?? null);
|
|
474
|
+
if (projection) this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
|
|
475
|
+
liveState: restartRequired ? "uncertain" : projection.liveState, operationRef: null,
|
|
476
|
+
reasonCode: restartRequired ? "runtime-restart-required" : null });
|
|
477
|
+
if (this.#active.get(sessionRef) === active) { active.completion = null;
|
|
478
|
+
if (!active.watchdog?.terminating) { active.stream = null; active.watchdog = null; active.settling = false; } }
|
|
479
|
+
if (!restartRequired || !projection || this.#active.get(sessionRef) !== active) return;
|
|
480
|
+
let restartAuthority: ActiveRuntime | null = active;
|
|
481
|
+
try {
|
|
482
|
+
await this.restart(sessionRef); restartAuthority = this.#active.get(sessionRef) ?? null;
|
|
483
|
+
const projection = await this.#readProjection?.(sessionRef);
|
|
484
|
+
if (!projection || !restartAuthority || this.#active.get(sessionRef) !== restartAuthority || this.ownership(sessionRef).state !== "gateway-owned") return;
|
|
485
|
+
this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision, liveState: projection.liveState, operationRef: null, reasonCode: null });
|
|
486
|
+
} catch {
|
|
487
|
+
if (!restartAuthority || this.#active.get(sessionRef) !== restartAuthority || this.ownership(sessionRef).state !== "gateway-owned") return;
|
|
488
|
+
this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision, liveState: "uncertain", operationRef: null, reasonCode: "runtime-restart-failed" });
|
|
469
489
|
}
|
|
470
490
|
}
|
|
471
|
-
|
|
472
491
|
async close(): Promise<void> {
|
|
473
492
|
if (this.#closed) return;
|
|
474
493
|
this.#closed = true;
|
|
@@ -56,6 +56,7 @@ export async function startLoopbackServer(options: {
|
|
|
56
56
|
mode?: "session" | "gateway";
|
|
57
57
|
readCapabilities: ReadCapabilities;
|
|
58
58
|
readSessionCatalog?: () => unknown | Promise<unknown>;
|
|
59
|
+
readSessionLiveState?: () => unknown | Promise<unknown>;
|
|
59
60
|
readSessionCreationOptions?: () => unknown | Promise<unknown>;
|
|
60
61
|
readSessionModel?: (sessionRef: string) => WebUiReadModelProvider | Promise<WebUiReadModelProvider>;
|
|
61
62
|
executeSessionAttachment?: (sessionRef: string, command: unknown) => unknown | Promise<unknown>;
|
|
@@ -296,6 +297,13 @@ export async function startLoopbackServer(options: {
|
|
|
296
297
|
catch { return errorResponse(response, 503, "session-catalog-unavailable"); }
|
|
297
298
|
}
|
|
298
299
|
|
|
300
|
+
if (request.method === "GET" && url.pathname === "/api/v1/session-live-state" && options.readSessionLiveState) {
|
|
301
|
+
if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
|
|
302
|
+
if (url.search) return errorResponse(response, 400, "invalid-session-live-state-query");
|
|
303
|
+
try { return jsonResponse(response, 200, await options.readSessionLiveState()); }
|
|
304
|
+
catch { return errorResponse(response, 503, "session-live-state-unavailable"); }
|
|
305
|
+
}
|
|
306
|
+
|
|
299
307
|
if (request.method === "GET" && url.pathname === "/api/v1/session-creation-options" && options.readSessionCreationOptions) {
|
|
300
308
|
if (!auth.authenticate(request)) return errorResponse(response, 401, "authentication-required");
|
|
301
309
|
if (url.search) return errorResponse(response, 400, "invalid-session-creation-query");
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
|
|
3
3
|
import { redactSensitiveText } from "../../piagent-core/extensions/redaction-core.js";
|
|
4
|
+
import { hasVisibleText } from "../shared/text-visibility.ts";
|
|
4
5
|
|
|
5
6
|
const MAX_ENTRIES = 50_000;
|
|
6
7
|
const MAX_ITEMS = 200;
|
|
7
8
|
const MAX_TEXT = 16_384;
|
|
8
|
-
const ANSI = /\u001b(
|
|
9
|
+
const ANSI = /\u001b(?:\[[0-?]*[ -/]*[@-~]|[@-_])/g;
|
|
10
|
+
const COMPLETION_GATE = /^\[Piagent completion gate: (CONTINUING|NOT APPROVED)\]/i;
|
|
9
11
|
|
|
10
12
|
type TranscriptIdentity = { projectRef: string; runtimeInstanceId: string; sessionRef: string; taskId: string | null; taskRunId: string | null;
|
|
11
13
|
agentOperationId: null; toolCallId: null };
|
|
@@ -87,6 +89,21 @@ function assistantFailureReason(message: any): string | null {
|
|
|
87
89
|
if (/(?:network|fetch failed|econn|timed? ?out|unavailable)/.test(detail)) return "provider-unavailable";
|
|
88
90
|
return "provider-response-failed";
|
|
89
91
|
}
|
|
92
|
+
function assistantUnavailableReason(message: any, text: string, projectedToolCalls: TranscriptItem["toolCalls"]): string | null {
|
|
93
|
+
if (message?.role !== "assistant") return null;
|
|
94
|
+
const stopReason = String(message?.stopReason ?? "");
|
|
95
|
+
if (stopReason === "error") return assistantFailureReason(message) ?? "provider-response-failed";
|
|
96
|
+
if (stopReason === "aborted") return "assistant-message-aborted";
|
|
97
|
+
if (stopReason === "length") return "assistant-output-incomplete";
|
|
98
|
+
const gate = COMPLETION_GATE.exec(text.trimStart());
|
|
99
|
+
if (gate) return gate[1]?.toUpperCase() === "CONTINUING"
|
|
100
|
+
? "assistant-completion-continuing" : "assistant-completion-not-approved";
|
|
101
|
+
if (projectedToolCalls.length > 0 || ["toolUse", "pending", "deferred"].includes(stopReason)) return "assistant-intermediate-output";
|
|
102
|
+
if (!stopReason) return "assistant-settlement-unknown";
|
|
103
|
+
if (stopReason && stopReason !== "stop") return "assistant-settlement-unknown";
|
|
104
|
+
if (!hasVisibleText(text)) return "assistant-message-empty";
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
90
107
|
function safeText(value: string): { full: string; preview: string; redacted: boolean; truncated: boolean } {
|
|
91
108
|
const clean = value.replace(ANSI, "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, " ");
|
|
92
109
|
const redaction = redactSensitiveText(clean);
|
|
@@ -129,17 +146,18 @@ function item(entry: any, identity: TranscriptIdentity): TranscriptItem | null {
|
|
|
129
146
|
const itemRole = role(entry.message), recordedAt = timestamp(entry.timestamp ?? entry.message.timestamp);
|
|
130
147
|
if (!itemRole || !recordedAt || typeof entry.id !== "string" || entry.id.length === 0) return null;
|
|
131
148
|
const userProjection = userMessageProjection(entry.message);
|
|
149
|
+
const projectedToolCalls = toolCalls(entry.message, identity.sessionRef, itemRole);
|
|
132
150
|
const content = itemRole === "tool-result" ? unavailableContent("tool-output-in-activity-preview") : (() => {
|
|
133
151
|
const projected = safeText(itemRole === "user" ? userProjection.text : messageText(entry.message));
|
|
134
|
-
const
|
|
135
|
-
if (
|
|
152
|
+
const unavailableReason = assistantUnavailableReason(entry.message, projected.full, projectedToolCalls);
|
|
153
|
+
if (unavailableReason) return unavailableContent(unavailableReason);
|
|
136
154
|
return { state: projected.redacted ? "redacted" as const : "available" as const, text: projected.preview,
|
|
137
155
|
textChars: Math.min(1_000_000_000, projected.full.length), digest: `sha256:${hash(projected.full)}`, truncated: projected.truncated,
|
|
138
156
|
redacted: projected.redacted, imageCount: imageCount(entry.message), reasonCode: projected.redacted ? "sensitive-values-redacted" as const : null };
|
|
139
157
|
})();
|
|
140
158
|
return { messageRef: opaque("message", [identity.sessionRef, entry.id]), parentMessageRef: null, role: itemRole, recordedAt,
|
|
141
159
|
agentOperationId: null, turnIndex: null, content, ...(userProjection.attachments.length ? { attachments: userProjection.attachments } : {}),
|
|
142
|
-
toolCalls:
|
|
160
|
+
toolCalls: projectedToolCalls };
|
|
143
161
|
}
|
|
144
162
|
function unavailable(input: TranscriptProjectionInput, reasonCode: string, limit: number): TranscriptDocument {
|
|
145
163
|
return { schemaVersion: 1, version: "piagent-webui-transcript-v1", generatedAt: input.generatedAt ?? new Date().toISOString(),
|