@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
|
@@ -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,35 +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
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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);
|
|
308
313
|
}
|
|
309
|
-
|
|
310
314
|
#bindApproval(sessionRef: string, active: ActiveRuntime): void {
|
|
311
315
|
const authority = (): ApprovalAuthority => {
|
|
312
316
|
if (!active.operationRef || active.lease.state !== "gateway-owned" || !active.lease.revision) return null;
|
|
@@ -331,50 +335,53 @@ export class SessionRuntimeSupervisor {
|
|
|
331
335
|
active.unbindApproval = piApprovalBroker.bind({ cwd: active.info.cwd, rawSessionId: active.info.id,
|
|
332
336
|
runtimeInstanceId: active.lease.runtimeInstanceRef!, authority });
|
|
333
337
|
active.unsubscribeApproval = piApprovalBroker.subscribe(active.info.cwd, active.info.id, (event: ApprovalBrokerEvent) => {
|
|
338
|
+
active.watchdog?.progress();
|
|
334
339
|
const projection = piApprovalBroker.projection(active.info.cwd, active.info.id);
|
|
335
340
|
active.approvalWaiting = projection.summary.state === "waiting";
|
|
336
341
|
void this.#publishApprovalState(sessionRef, active, event);
|
|
337
342
|
});
|
|
338
343
|
}
|
|
339
|
-
|
|
340
344
|
async #publishApprovalState(sessionRef: string, active: ActiveRuntime, event: ApprovalBrokerEvent): Promise<void> {
|
|
341
345
|
try {
|
|
342
346
|
const projection = await this.#readProjection?.(sessionRef);
|
|
343
347
|
if (!projection || this.#active.get(sessionRef) !== active) return;
|
|
348
|
+
active.lastSessionRevision = projection.sessionRevision;
|
|
344
349
|
this.#events.publish("runtime.changed", { sessionRef, sessionRevision: projection.sessionRevision,
|
|
345
350
|
liveState: active.approvalWaiting ? "waiting-approval" : active.operationRef ? "running" : projection.liveState,
|
|
346
351
|
operationRef: active.operationRef, reasonCode: `approval-${event.kind}` });
|
|
347
352
|
} catch { /* approval truth remains available through the canonical inspection route */ }
|
|
348
353
|
}
|
|
349
|
-
|
|
350
354
|
async setModel(sessionRef: string, modelRef: string): Promise<"model-changed" | "no-change"> {
|
|
351
355
|
await this.acquire(sessionRef);
|
|
352
356
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
353
357
|
if (!active || !session) throw new Error("session-runtime-unavailable");
|
|
354
358
|
if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
|
|
355
|
-
const current = session
|
|
356
|
-
? webUiModelRef(String(session.model.provider ?? ""), String(session.model.id ?? session.model.modelId ?? "")) : null;
|
|
359
|
+
const current = effectiveModelRef(session);
|
|
357
360
|
if (current === modelRef) return "no-change";
|
|
358
361
|
const models = session.modelRuntime?.getAvailableSnapshot?.() ?? [];
|
|
359
|
-
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);
|
|
360
363
|
if (!model) throw new Error("session-model-unavailable");
|
|
361
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");
|
|
362
368
|
this.#touchCreated(sessionRef);
|
|
363
369
|
return "model-changed";
|
|
364
370
|
}
|
|
365
|
-
|
|
366
371
|
async setThinking(sessionRef: string, thinkingLevel: string): Promise<"thinking-changed" | "no-change"> {
|
|
367
372
|
await this.acquire(sessionRef);
|
|
368
373
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
369
374
|
if (!active || !session) throw new Error("session-runtime-unavailable");
|
|
370
375
|
if (active.operationRef || !session.isIdle) throw new Error("session-runtime-busy");
|
|
371
|
-
const current =
|
|
376
|
+
const current = effectiveThinkingLevel(session);
|
|
372
377
|
if (current === thinkingLevel) return "no-change";
|
|
373
|
-
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");
|
|
374
382
|
this.#touchCreated(sessionRef);
|
|
375
383
|
return "thinking-changed";
|
|
376
384
|
}
|
|
377
|
-
|
|
378
385
|
async setPermission(sessionRef: string, permissionMode: "read-only" | "workspace-write" | "trusted-full-access"): Promise<"permission-changed"> {
|
|
379
386
|
await this.acquire(sessionRef);
|
|
380
387
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
@@ -384,7 +391,6 @@ export class SessionRuntimeSupervisor {
|
|
|
384
391
|
this.#touchCreated(sessionRef);
|
|
385
392
|
return "permission-changed";
|
|
386
393
|
}
|
|
387
|
-
|
|
388
394
|
async runRuntimeCommand(sessionRef: string, command: string): Promise<RuntimeCommandResult> {
|
|
389
395
|
await this.acquire(sessionRef);
|
|
390
396
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
@@ -398,12 +404,10 @@ export class SessionRuntimeSupervisor {
|
|
|
398
404
|
health: { state: "unavailable", reasonCode: "approval-broker-unavailable", message: "Approval broker is unavailable" } } };
|
|
399
405
|
return piApprovalBroker.projection(active.info.cwd, active.info.id);
|
|
400
406
|
}
|
|
401
|
-
|
|
402
407
|
approvalDetail(sessionRef: string, approvalRef: string): unknown | null {
|
|
403
408
|
const active = this.#active.get(sessionRef);
|
|
404
409
|
return active ? piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef) : null;
|
|
405
410
|
}
|
|
406
|
-
|
|
407
411
|
async decideApproval(approvalRef: string, decision: unknown): Promise<unknown> {
|
|
408
412
|
for (const active of this.#active.values()) {
|
|
409
413
|
if (piApprovalBroker.detail(active.info.cwd, active.info.id, approvalRef)) {
|
|
@@ -412,7 +416,6 @@ export class SessionRuntimeSupervisor {
|
|
|
412
416
|
}
|
|
413
417
|
throw new Error("approval-not-pending");
|
|
414
418
|
}
|
|
415
|
-
|
|
416
419
|
async rename(sessionRef: string, title: string): Promise<"renamed" | "no-change"> {
|
|
417
420
|
await this.acquire(sessionRef);
|
|
418
421
|
const active = this.#active.get(sessionRef), session = active?.runtime.session;
|
|
@@ -424,7 +427,6 @@ export class SessionRuntimeSupervisor {
|
|
|
424
427
|
this.#touchCreated(sessionRef);
|
|
425
428
|
return "renamed";
|
|
426
429
|
}
|
|
427
|
-
|
|
428
430
|
async fork(sessionRef: string, entryRef: string | null, title: string | null): Promise<string> {
|
|
429
431
|
if (!this.#host) throw new Error("session-fork-unavailable");
|
|
430
432
|
await this.acquire(sessionRef);
|
|
@@ -451,28 +453,41 @@ export class SessionRuntimeSupervisor {
|
|
|
451
453
|
this.#created.set(forkRef, created);
|
|
452
454
|
return forkRef;
|
|
453
455
|
}
|
|
454
|
-
|
|
455
|
-
#touchCreated(sessionRef: string): void {
|
|
456
|
-
const created = this.#created.get(sessionRef);
|
|
457
|
-
if (created) created.modified = new Date();
|
|
458
|
-
}
|
|
459
|
-
|
|
456
|
+
#touchCreated(sessionRef: string): void { const created = this.#created.get(sessionRef); if (created) created.modified = new Date(); }
|
|
460
457
|
async #finishOperation(sessionRef: string, operationRef: string, stream: GatewaySessionStream): Promise<void> {
|
|
461
458
|
const active = this.#active.get(sessionRef);
|
|
462
459
|
if (!active || active.operationRef !== operationRef) return;
|
|
463
|
-
|
|
460
|
+
const restartRequired = stream.runtimeRestartRequired; let projection: Projection | null = null;
|
|
461
|
+
bestEffortUnsubscribe(active.unsubscribe); active.unsubscribe = null; active.watchdog?.close(); active.settling = true;
|
|
464
462
|
try {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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
|
+
}
|
|
470
468
|
} catch { /* Canonical refresh failure cannot be replaced by an invented revision. */ }
|
|
471
|
-
|
|
472
|
-
|
|
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" });
|
|
473
489
|
}
|
|
474
490
|
}
|
|
475
|
-
|
|
476
491
|
async close(): Promise<void> {
|
|
477
492
|
if (this.#closed) return;
|
|
478
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(),
|