lingxios 1.0.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/README.md +54 -0
- package/db/schema.sql +306 -0
- package/dist/src/app/artifacts.d.ts +8 -0
- package/dist/src/app/artifacts.js +114 -0
- package/dist/src/app/artifacts.js.map +1 -0
- package/dist/src/app/doctor.d.ts +22 -0
- package/dist/src/app/doctor.js +44 -0
- package/dist/src/app/doctor.js.map +1 -0
- package/dist/src/app/index.d.ts +123 -0
- package/dist/src/app/index.js +291 -0
- package/dist/src/app/index.js.map +1 -0
- package/dist/src/app/input.d.ts +22 -0
- package/dist/src/app/input.js +60 -0
- package/dist/src/app/input.js.map +1 -0
- package/dist/src/app/recover-wait.d.ts +5 -0
- package/dist/src/app/recover-wait.js +59 -0
- package/dist/src/app/recover-wait.js.map +1 -0
- package/dist/src/app/resources.d.ts +5 -0
- package/dist/src/app/resources.js +9 -0
- package/dist/src/app/resources.js.map +1 -0
- package/dist/src/app/storage.d.ts +2 -0
- package/dist/src/app/storage.js +41 -0
- package/dist/src/app/storage.js.map +1 -0
- package/dist/src/cli/doctor.d.ts +2 -0
- package/dist/src/cli/doctor.js +20 -0
- package/dist/src/cli/doctor.js.map +1 -0
- package/dist/src/cli/eval.d.ts +2 -0
- package/dist/src/cli/eval.js +44 -0
- package/dist/src/cli/eval.js.map +1 -0
- package/dist/src/config.d.ts +22 -0
- package/dist/src/config.js +56 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/context/attachments.d.ts +10 -0
- package/dist/src/context/attachments.js +18 -0
- package/dist/src/context/attachments.js.map +1 -0
- package/dist/src/context/document-text.d.ts +1 -0
- package/dist/src/context/document-text.js +29 -0
- package/dist/src/context/document-text.js.map +1 -0
- package/dist/src/context/document-worker.d.ts +1 -0
- package/dist/src/context/document-worker.js +62 -0
- package/dist/src/context/document-worker.js.map +1 -0
- package/dist/src/context/evidence.d.ts +19 -0
- package/dist/src/context/evidence.js +37 -0
- package/dist/src/context/evidence.js.map +1 -0
- package/dist/src/context/request.d.ts +21 -0
- package/dist/src/context/request.js +32 -0
- package/dist/src/context/request.js.map +1 -0
- package/dist/src/context/research-evidence.d.ts +4 -0
- package/dist/src/context/research-evidence.js +24 -0
- package/dist/src/context/research-evidence.js.map +1 -0
- package/dist/src/context/resource-checks.d.ts +7 -0
- package/dist/src/context/resource-checks.js +15 -0
- package/dist/src/context/resource-checks.js.map +1 -0
- package/dist/src/context/task-contract.d.ts +11 -0
- package/dist/src/context/task-contract.js +23 -0
- package/dist/src/context/task-contract.js.map +1 -0
- package/dist/src/context/text.d.ts +2 -0
- package/dist/src/context/text.js +7 -0
- package/dist/src/context/text.js.map +1 -0
- package/dist/src/control-plane/http-server.d.ts +22 -0
- package/dist/src/control-plane/http-server.js +196 -0
- package/dist/src/control-plane/http-server.js.map +1 -0
- package/dist/src/control-plane/memory-store.d.ts +71 -0
- package/dist/src/control-plane/memory-store.js +389 -0
- package/dist/src/control-plane/memory-store.js.map +1 -0
- package/dist/src/control-plane/pg-store.d.ts +62 -0
- package/dist/src/control-plane/pg-store.js +386 -0
- package/dist/src/control-plane/pg-store.js.map +1 -0
- package/dist/src/control-plane/service.d.ts +52 -0
- package/dist/src/control-plane/service.js +630 -0
- package/dist/src/control-plane/service.js.map +1 -0
- package/dist/src/control-plane/stores.d.ts +184 -0
- package/dist/src/control-plane/stores.js +19 -0
- package/dist/src/control-plane/stores.js.map +1 -0
- package/dist/src/errors.d.ts +66 -0
- package/dist/src/errors.js +102 -0
- package/dist/src/errors.js.map +1 -0
- package/dist/src/eval/calibration.d.ts +16 -0
- package/dist/src/eval/calibration.js +28 -0
- package/dist/src/eval/calibration.js.map +1 -0
- package/dist/src/eval/index.d.ts +40 -0
- package/dist/src/eval/index.js +40 -0
- package/dist/src/eval/index.js.map +1 -0
- package/dist/src/eval/review.d.ts +20 -0
- package/dist/src/eval/review.js +49 -0
- package/dist/src/eval/review.js.map +1 -0
- package/dist/src/host/http-client.d.ts +48 -0
- package/dist/src/host/http-client.js +127 -0
- package/dist/src/host/http-client.js.map +1 -0
- package/dist/src/host/port.d.ts +26 -0
- package/dist/src/host/port.js +2 -0
- package/dist/src/host/port.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.js +9 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/integrations/lingxiloop/actions.d.ts +25 -0
- package/dist/src/integrations/lingxiloop/actions.js +77 -0
- package/dist/src/integrations/lingxiloop/actions.js.map +1 -0
- package/dist/src/integrations/lingxiloop/app.d.ts +167 -0
- package/dist/src/integrations/lingxiloop/app.js +434 -0
- package/dist/src/integrations/lingxiloop/app.js.map +1 -0
- package/dist/src/integrations/lingxiloop/approvals.d.ts +148 -0
- package/dist/src/integrations/lingxiloop/approvals.js +540 -0
- package/dist/src/integrations/lingxiloop/approvals.js.map +1 -0
- package/dist/src/integrations/lingxiloop/attachments.d.ts +8 -0
- package/dist/src/integrations/lingxiloop/attachments.js +38 -0
- package/dist/src/integrations/lingxiloop/attachments.js.map +1 -0
- package/dist/src/integrations/lingxiloop/calendar-approvals.d.ts +21 -0
- package/dist/src/integrations/lingxiloop/calendar-approvals.js +126 -0
- package/dist/src/integrations/lingxiloop/calendar-approvals.js.map +1 -0
- package/dist/src/integrations/lingxiloop/calendar-writes.d.ts +15 -0
- package/dist/src/integrations/lingxiloop/calendar-writes.js +79 -0
- package/dist/src/integrations/lingxiloop/calendar-writes.js.map +1 -0
- package/dist/src/integrations/lingxiloop/calendar.d.ts +19 -0
- package/dist/src/integrations/lingxiloop/calendar.js +43 -0
- package/dist/src/integrations/lingxiloop/calendar.js.map +1 -0
- package/dist/src/integrations/lingxiloop/canvas-events.d.ts +7 -0
- package/dist/src/integrations/lingxiloop/canvas-events.js +53 -0
- package/dist/src/integrations/lingxiloop/canvas-events.js.map +1 -0
- package/dist/src/integrations/lingxiloop/canvas-summary.d.ts +5 -0
- package/dist/src/integrations/lingxiloop/canvas-summary.js +52 -0
- package/dist/src/integrations/lingxiloop/canvas-summary.js.map +1 -0
- package/dist/src/integrations/lingxiloop/canvas-work-report.d.ts +12 -0
- package/dist/src/integrations/lingxiloop/canvas-work-report.js +120 -0
- package/dist/src/integrations/lingxiloop/canvas-work-report.js.map +1 -0
- package/dist/src/integrations/lingxiloop/canvas-work.d.ts +11 -0
- package/dist/src/integrations/lingxiloop/canvas-work.js +306 -0
- package/dist/src/integrations/lingxiloop/canvas-work.js.map +1 -0
- package/dist/src/integrations/lingxiloop/canvas.d.ts +12 -0
- package/dist/src/integrations/lingxiloop/canvas.js +79 -0
- package/dist/src/integrations/lingxiloop/canvas.js.map +1 -0
- package/dist/src/integrations/lingxiloop/chat.d.ts +11 -0
- package/dist/src/integrations/lingxiloop/chat.js +46 -0
- package/dist/src/integrations/lingxiloop/chat.js.map +1 -0
- package/dist/src/integrations/lingxiloop/document-approvals.d.ts +21 -0
- package/dist/src/integrations/lingxiloop/document-approvals.js +75 -0
- package/dist/src/integrations/lingxiloop/document-approvals.js.map +1 -0
- package/dist/src/integrations/lingxiloop/document-content.d.ts +80 -0
- package/dist/src/integrations/lingxiloop/document-content.js +195 -0
- package/dist/src/integrations/lingxiloop/document-content.js.map +1 -0
- package/dist/src/integrations/lingxiloop/document-writes.d.ts +8 -0
- package/dist/src/integrations/lingxiloop/document-writes.js +49 -0
- package/dist/src/integrations/lingxiloop/document-writes.js.map +1 -0
- package/dist/src/integrations/lingxiloop/documents.d.ts +22 -0
- package/dist/src/integrations/lingxiloop/documents.js +38 -0
- package/dist/src/integrations/lingxiloop/documents.js.map +1 -0
- package/dist/src/integrations/lingxiloop/event-outbox.d.ts +5 -0
- package/dist/src/integrations/lingxiloop/event-outbox.js +47 -0
- package/dist/src/integrations/lingxiloop/event-outbox.js.map +1 -0
- package/dist/src/integrations/lingxiloop/index.d.ts +3 -0
- package/dist/src/integrations/lingxiloop/index.js +3 -0
- package/dist/src/integrations/lingxiloop/index.js.map +1 -0
- package/dist/src/integrations/lingxiloop/learning-attempts.d.ts +13 -0
- package/dist/src/integrations/lingxiloop/learning-attempts.js +58 -0
- package/dist/src/integrations/lingxiloop/learning-attempts.js.map +1 -0
- package/dist/src/integrations/lingxiloop/learning-evaluation.d.ts +8 -0
- package/dist/src/integrations/lingxiloop/learning-evaluation.js +64 -0
- package/dist/src/integrations/lingxiloop/learning-evaluation.js.map +1 -0
- package/dist/src/integrations/lingxiloop/learning-evidence.d.ts +7 -0
- package/dist/src/integrations/lingxiloop/learning-evidence.js +88 -0
- package/dist/src/integrations/lingxiloop/learning-evidence.js.map +1 -0
- package/dist/src/integrations/lingxiloop/learning-missions.d.ts +18 -0
- package/dist/src/integrations/lingxiloop/learning-missions.js +95 -0
- package/dist/src/integrations/lingxiloop/learning-missions.js.map +1 -0
- package/dist/src/integrations/lingxiloop/learning.d.ts +22 -0
- package/dist/src/integrations/lingxiloop/learning.js +150 -0
- package/dist/src/integrations/lingxiloop/learning.js.map +1 -0
- package/dist/src/integrations/lingxiloop/memory.d.ts +14 -0
- package/dist/src/integrations/lingxiloop/memory.js +133 -0
- package/dist/src/integrations/lingxiloop/memory.js.map +1 -0
- package/dist/src/integrations/lingxiloop/polls.d.ts +9 -0
- package/dist/src/integrations/lingxiloop/polls.js +47 -0
- package/dist/src/integrations/lingxiloop/polls.js.map +1 -0
- package/dist/src/integrations/lingxiloop/presentations.d.ts +11 -0
- package/dist/src/integrations/lingxiloop/presentations.js +34 -0
- package/dist/src/integrations/lingxiloop/presentations.js.map +1 -0
- package/dist/src/integrations/lingxiloop/questionnaire.d.ts +21 -0
- package/dist/src/integrations/lingxiloop/questionnaire.js +57 -0
- package/dist/src/integrations/lingxiloop/questionnaire.js.map +1 -0
- package/dist/src/integrations/lingxiloop/research.d.ts +19 -0
- package/dist/src/integrations/lingxiloop/research.js +20 -0
- package/dist/src/integrations/lingxiloop/research.js.map +1 -0
- package/dist/src/integrations/lingxiloop/routines.d.ts +91 -0
- package/dist/src/integrations/lingxiloop/routines.js +198 -0
- package/dist/src/integrations/lingxiloop/routines.js.map +1 -0
- package/dist/src/integrations/lingxiloop/service-contracts.d.ts +1045 -0
- package/dist/src/integrations/lingxiloop/service-contracts.js +2 -0
- package/dist/src/integrations/lingxiloop/service-contracts.js.map +1 -0
- package/dist/src/integrations/lingxiloop/teacher-context.d.ts +32 -0
- package/dist/src/integrations/lingxiloop/teacher-context.js +25 -0
- package/dist/src/integrations/lingxiloop/teacher-context.js.map +1 -0
- package/dist/src/integrations/lingxiloop/teacher-digest.d.ts +13 -0
- package/dist/src/integrations/lingxiloop/teacher-digest.js +152 -0
- package/dist/src/integrations/lingxiloop/teacher-digest.js.map +1 -0
- package/dist/src/integrations/lingxiloop/teacher-preview.d.ts +22 -0
- package/dist/src/integrations/lingxiloop/teacher-preview.js +41 -0
- package/dist/src/integrations/lingxiloop/teacher-preview.js.map +1 -0
- package/dist/src/integrations/lingxiloop/teacher.d.ts +24 -0
- package/dist/src/integrations/lingxiloop/teacher.js +224 -0
- package/dist/src/integrations/lingxiloop/teacher.js.map +1 -0
- package/dist/src/kernel/manager.d.ts +52 -0
- package/dist/src/kernel/manager.js +407 -0
- package/dist/src/kernel/manager.js.map +1 -0
- package/dist/src/logging.d.ts +21 -0
- package/dist/src/logging.js +54 -0
- package/dist/src/logging.js.map +1 -0
- package/dist/src/memory/evidence.d.ts +6 -0
- package/dist/src/memory/evidence.js +43 -0
- package/dist/src/memory/evidence.js.map +1 -0
- package/dist/src/memory/processor.d.ts +3 -0
- package/dist/src/memory/processor.js +66 -0
- package/dist/src/memory/processor.js.map +1 -0
- package/dist/src/memory/semantic.d.ts +19 -0
- package/dist/src/memory/semantic.js +93 -0
- package/dist/src/memory/semantic.js.map +1 -0
- package/dist/src/memory/store.d.ts +20 -0
- package/dist/src/memory/store.js +34 -0
- package/dist/src/memory/store.js.map +1 -0
- package/dist/src/memory/synthesis.d.ts +24 -0
- package/dist/src/memory/synthesis.js +138 -0
- package/dist/src/memory/synthesis.js.map +1 -0
- package/dist/src/metrics.d.ts +43 -0
- package/dist/src/metrics.js +130 -0
- package/dist/src/metrics.js.map +1 -0
- package/dist/src/model/driver.d.ts +60 -0
- package/dist/src/model/driver.js +2 -0
- package/dist/src/model/driver.js.map +1 -0
- package/dist/src/model/embeddings.d.ts +24 -0
- package/dist/src/model/embeddings.js +97 -0
- package/dist/src/model/embeddings.js.map +1 -0
- package/dist/src/model/openai.d.ts +45 -0
- package/dist/src/model/openai.js +340 -0
- package/dist/src/model/openai.js.map +1 -0
- package/dist/src/outcome/assessment.d.ts +16 -0
- package/dist/src/outcome/assessment.js +45 -0
- package/dist/src/outcome/assessment.js.map +1 -0
- package/dist/src/outcome/content-check.d.ts +19 -0
- package/dist/src/outcome/content-check.js +44 -0
- package/dist/src/outcome/content-check.js.map +1 -0
- package/dist/src/outcome/envelope.d.ts +34 -0
- package/dist/src/outcome/envelope.js +42 -0
- package/dist/src/outcome/envelope.js.map +1 -0
- package/dist/src/protocol/constants.d.ts +25 -0
- package/dist/src/protocol/constants.js +26 -0
- package/dist/src/protocol/constants.js.map +1 -0
- package/dist/src/protocol/kernel-wire.d.ts +74 -0
- package/dist/src/protocol/kernel-wire.js +2 -0
- package/dist/src/protocol/kernel-wire.js.map +1 -0
- package/dist/src/protocol/outcome.d.ts +16 -0
- package/dist/src/protocol/outcome.js +14 -0
- package/dist/src/protocol/outcome.js.map +1 -0
- package/dist/src/protocol/types.d.ts +266 -0
- package/dist/src/protocol/types.js +19 -0
- package/dist/src/protocol/types.js.map +1 -0
- package/dist/src/research/address.d.ts +2 -0
- package/dist/src/research/address.js +32 -0
- package/dist/src/research/address.js.map +1 -0
- package/dist/src/research/fetch.d.ts +7 -0
- package/dist/src/research/fetch.js +83 -0
- package/dist/src/research/fetch.js.map +1 -0
- package/dist/src/research/index.d.ts +26 -0
- package/dist/src/research/index.js +109 -0
- package/dist/src/research/index.js.map +1 -0
- package/dist/src/runtime/compaction.d.ts +41 -0
- package/dist/src/runtime/compaction.js +86 -0
- package/dist/src/runtime/compaction.js.map +1 -0
- package/dist/src/runtime/corrections.d.ts +20 -0
- package/dist/src/runtime/corrections.js +27 -0
- package/dist/src/runtime/corrections.js.map +1 -0
- package/dist/src/runtime/policy.d.ts +42 -0
- package/dist/src/runtime/policy.js +91 -0
- package/dist/src/runtime/policy.js.map +1 -0
- package/dist/src/runtime/resource-refresh.d.ts +8 -0
- package/dist/src/runtime/resource-refresh.js +37 -0
- package/dist/src/runtime/resource-refresh.js.map +1 -0
- package/dist/src/runtime/runtime.d.ts +51 -0
- package/dist/src/runtime/runtime.js +804 -0
- package/dist/src/runtime/runtime.js.map +1 -0
- package/dist/src/runtime/tool.d.ts +13 -0
- package/dist/src/runtime/tool.js +42 -0
- package/dist/src/runtime/tool.js.map +1 -0
- package/dist/src/ui/index.d.ts +27 -0
- package/dist/src/ui/index.js +54 -0
- package/dist/src/ui/index.js.map +1 -0
- package/dist/src/versions.d.ts +9 -0
- package/dist/src/versions.js +11 -0
- package/dist/src/versions.js.map +1 -0
- package/dist/src/worker/index.d.ts +3 -0
- package/dist/src/worker/index.js +57 -0
- package/dist/src/worker/index.js.map +1 -0
- package/dist/src/worker/main.d.ts +2 -0
- package/dist/src/worker/main.js +9 -0
- package/dist/src/worker/main.js.map +1 -0
- package/dist/src/worker/worker.d.ts +45 -0
- package/dist/src/worker/worker.js +154 -0
- package/dist/src/worker/worker.js.map +1 -0
- package/kernel/runner.py +507 -0
- package/package.json +83 -0
|
@@ -0,0 +1,804 @@
|
|
|
1
|
+
import { appendResearchEvidence } from '../context/research-evidence.js';
|
|
2
|
+
import { appendResourceCheck } from '../context/resource-checks.js';
|
|
3
|
+
import { createTaskContract } from '../context/task-contract.js';
|
|
4
|
+
/**
|
|
5
|
+
* AgentRuntime — the product-agnostic agent loop.
|
|
6
|
+
*
|
|
7
|
+
* One `runWork` call executes one lease attempt of one work item:
|
|
8
|
+
*
|
|
9
|
+
* claim (done by caller) → load context → restore session → model/kernel
|
|
10
|
+
* hops → validated final text → commit message → complete.
|
|
11
|
+
*
|
|
12
|
+
* Everything durable flows through the {@link HostPort}; everything
|
|
13
|
+
* product-specific flows through the {@link RuntimePolicy}. Non-`turn` work
|
|
14
|
+
* kinds are dispatched to registered {@link WorkProcessor}s.
|
|
15
|
+
*
|
|
16
|
+
* Attempt identity: the run id is the work id. A retried attempt reuses every
|
|
17
|
+
* externally visible identity and writes its events into the fence-scoped
|
|
18
|
+
* sequence range, so earlier attempts' events are never shadowed or deduped
|
|
19
|
+
* against it.
|
|
20
|
+
*/
|
|
21
|
+
import { ApprovalPendingError, KernelCancelledError, KernelExecutionError, KernelTimeoutError, LeaseLostError, ModelDriverError, RunCancelledError, errorMessage, } from '../errors.js';
|
|
22
|
+
import { requestItems, snapshotRequest } from '../context/request.js';
|
|
23
|
+
import { evidenceItems, snapshotEvidence } from '../context/evidence.js';
|
|
24
|
+
import { createResponseEnvelope } from '../outcome/envelope.js';
|
|
25
|
+
import { checkCandidateContent } from '../outcome/content-check.js';
|
|
26
|
+
import { parseFinalCandidate } from '../outcome/assessment.js';
|
|
27
|
+
import { nullLogger } from '../logging.js';
|
|
28
|
+
import { RUN_SEQUENCE_SPAN } from '../protocol/constants.js';
|
|
29
|
+
import { sessionKeyOf, actionKeyOf, } from '../protocol/types.js';
|
|
30
|
+
import { compactIfNeeded, DEFAULT_COMPACTION, estimateTokens, HardLimitExceededError } from './compaction.js';
|
|
31
|
+
import { CorrectionBudget } from './corrections.js';
|
|
32
|
+
import { refreshResourceChecks } from './resource-refresh.js';
|
|
33
|
+
import { DefaultRuntimePolicy } from './policy.js';
|
|
34
|
+
import { boundedToolOutput, parseIPythonArguments } from './tool.js';
|
|
35
|
+
export class AgentRuntime {
|
|
36
|
+
host;
|
|
37
|
+
model;
|
|
38
|
+
kernels;
|
|
39
|
+
policy;
|
|
40
|
+
maxHops;
|
|
41
|
+
heartbeatMs;
|
|
42
|
+
compaction;
|
|
43
|
+
logger;
|
|
44
|
+
promptContractVersion;
|
|
45
|
+
processors = new Map();
|
|
46
|
+
eventSeqByRun = new Map();
|
|
47
|
+
constructor(host, model, kernels, options = {}) {
|
|
48
|
+
this.host = host;
|
|
49
|
+
this.model = model;
|
|
50
|
+
this.kernels = kernels;
|
|
51
|
+
this.policy = options.policy ?? new DefaultRuntimePolicy();
|
|
52
|
+
this.maxHops = options.maxHops ?? 12;
|
|
53
|
+
if (!Number.isSafeInteger(this.maxHops) || this.maxHops < 1)
|
|
54
|
+
throw new Error('maxHops must be a positive integer');
|
|
55
|
+
this.heartbeatMs = options.heartbeatMs ?? 5_000;
|
|
56
|
+
this.compaction = { ...DEFAULT_COMPACTION, ...(model.contextWindowTokens ? { contextWindowTokens: model.contextWindowTokens } : {}), ...options.compaction };
|
|
57
|
+
this.logger = options.logger ?? nullLogger;
|
|
58
|
+
this.promptContractVersion = options.promptContractVersion ?? 'prompt-v5';
|
|
59
|
+
}
|
|
60
|
+
/** Register a custom processor or the normal conversation pipeline for a work kind. */
|
|
61
|
+
registerProcessor(kind, processor) {
|
|
62
|
+
this.processors.set(kind, processor);
|
|
63
|
+
}
|
|
64
|
+
async event(work, runId, event) {
|
|
65
|
+
const seq = (this.eventSeqByRun.get(runId) ?? 0) + 1;
|
|
66
|
+
this.eventSeqByRun.set(runId, seq);
|
|
67
|
+
await this.host.emitEvent(work, { runId, seq, ...event });
|
|
68
|
+
return seq;
|
|
69
|
+
}
|
|
70
|
+
startSignals(work, external) {
|
|
71
|
+
const lifecycle = new AbortController();
|
|
72
|
+
let leaseLost = null;
|
|
73
|
+
let preemptRequested = false;
|
|
74
|
+
const steerQueue = [];
|
|
75
|
+
const seenSteer = new Set();
|
|
76
|
+
let heartbeatInFlight;
|
|
77
|
+
const abortFromCaller = () => lifecycle.abort(external?.reason ?? new RunCancelledError('caller'));
|
|
78
|
+
if (external?.aborted)
|
|
79
|
+
abortFromCaller();
|
|
80
|
+
else
|
|
81
|
+
external?.addEventListener('abort', abortFromCaller, { once: true });
|
|
82
|
+
const refresh = () => {
|
|
83
|
+
if (heartbeatInFlight)
|
|
84
|
+
return heartbeatInFlight;
|
|
85
|
+
heartbeatInFlight = this.host.heartbeat(work).then((result) => {
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
leaseLost = new LeaseLostError();
|
|
88
|
+
lifecycle.abort(leaseLost);
|
|
89
|
+
}
|
|
90
|
+
if (result.cancelRequested)
|
|
91
|
+
lifecycle.abort(new RunCancelledError('user'));
|
|
92
|
+
if (result.preemptRequested) {
|
|
93
|
+
preemptRequested = true;
|
|
94
|
+
lifecycle.abort(new RunCancelledError('preempted'));
|
|
95
|
+
}
|
|
96
|
+
for (const steer of result.steer ?? []) {
|
|
97
|
+
if (!seenSteer.has(steer.id)) {
|
|
98
|
+
seenSteer.add(steer.id);
|
|
99
|
+
steerQueue.push(steer);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}).catch((error) => {
|
|
103
|
+
leaseLost = error instanceof Error ? error : new LeaseLostError(String(error));
|
|
104
|
+
lifecycle.abort(leaseLost);
|
|
105
|
+
}).finally(() => { heartbeatInFlight = undefined; });
|
|
106
|
+
return heartbeatInFlight;
|
|
107
|
+
};
|
|
108
|
+
const heartbeat = setInterval(() => { void refresh(); }, this.heartbeatMs);
|
|
109
|
+
heartbeat.unref?.();
|
|
110
|
+
return {
|
|
111
|
+
lifecycle,
|
|
112
|
+
leaseLost: () => leaseLost,
|
|
113
|
+
preemptRequested: () => preemptRequested,
|
|
114
|
+
refresh,
|
|
115
|
+
hasSteer: () => steerQueue.length > 0,
|
|
116
|
+
drainSteer: () => steerQueue.splice(0),
|
|
117
|
+
stop: () => {
|
|
118
|
+
clearInterval(heartbeat);
|
|
119
|
+
external?.removeEventListener('abort', abortFromCaller);
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
async runWork(work, signal) {
|
|
124
|
+
const runId = work.id;
|
|
125
|
+
this.eventSeqByRun.set(runId, Math.max(0, work.fence - 1) * RUN_SEQUENCE_SPAN);
|
|
126
|
+
const signals = this.startSignals(work, signal);
|
|
127
|
+
let activeSession = null;
|
|
128
|
+
const log = this.logger.child({ runId, workId: work.id, agentId: work.agentId, fence: work.fence });
|
|
129
|
+
try {
|
|
130
|
+
await this.event(work, runId, {
|
|
131
|
+
kind: 'run.started', stage: 'started', visibility: 'user',
|
|
132
|
+
data: {
|
|
133
|
+
kind: work.kind, lane: work.lane, attempts: work.attempts ?? 1, preemptions: work.preemptions ?? 0,
|
|
134
|
+
...(work.availableAt ? { queueWaitMs: Math.max(0, Date.now() - Date.parse(work.availableAt)) } : {}),
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
const processor = work.kind === 'turn' || work.kind === 'resume' ? 'conversation' : this.processors.get(work.kind);
|
|
138
|
+
if (!processor)
|
|
139
|
+
throw new Error(`no processor registered for work kind '${work.kind}'`);
|
|
140
|
+
if (processor !== 'conversation') {
|
|
141
|
+
await processor.process(work, {
|
|
142
|
+
host: this.host,
|
|
143
|
+
model: this.model,
|
|
144
|
+
signal: signals.lifecycle.signal,
|
|
145
|
+
emit: async (event) => { await this.event(work, runId, event); },
|
|
146
|
+
});
|
|
147
|
+
await this.event(work, runId, { kind: 'run.completed', stage: 'completed', visibility: 'internal', data: {} });
|
|
148
|
+
await this.host.completeWork(work, { status: 'completed' });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const sessionRef = { session: null };
|
|
152
|
+
try {
|
|
153
|
+
await this.runTurn(work, runId, signals, log, sessionRef);
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
activeSession = sessionRef.session;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
await this.finishWithError(work, runId, signals, activeSession, error);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
signals.stop();
|
|
165
|
+
this.eventSeqByRun.delete(runId);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// -------------------------------------------------------------------------
|
|
169
|
+
// The turn loop
|
|
170
|
+
// -------------------------------------------------------------------------
|
|
171
|
+
async runTurn(work, runId, signals, log, sessionRef) {
|
|
172
|
+
const context = await this.host.loadContext(work);
|
|
173
|
+
await this.event(work, runId, {
|
|
174
|
+
kind: 'input.loaded', stage: 'completed', visibility: 'internal',
|
|
175
|
+
data: { triggerRef: work.triggerRef },
|
|
176
|
+
});
|
|
177
|
+
const session = await this.restoreSession(work, context);
|
|
178
|
+
sessionRef.session = session;
|
|
179
|
+
await this.host.saveSession(work, session);
|
|
180
|
+
const capabilities = this.policy.kernelCapabilities(context);
|
|
181
|
+
const budget = new CorrectionBudget();
|
|
182
|
+
let nextStreamPartIndex = 0;
|
|
183
|
+
let streamedText = '';
|
|
184
|
+
let finalText = '';
|
|
185
|
+
let fallbackText;
|
|
186
|
+
let finalEnvelope;
|
|
187
|
+
let contentCheckExhausted = false;
|
|
188
|
+
let acceptanceGaps = [];
|
|
189
|
+
const artifacts = [];
|
|
190
|
+
const evidence = () => session.request?.evidence ?? snapshotEvidence(`${work.id}:evidence:1`, []);
|
|
191
|
+
let protocolCorrection = null;
|
|
192
|
+
const applySteering = async () => {
|
|
193
|
+
const steers = signals.drainSteer();
|
|
194
|
+
if (steers.length > 0) {
|
|
195
|
+
fallbackText = undefined;
|
|
196
|
+
acceptanceGaps = [];
|
|
197
|
+
if (session.request) {
|
|
198
|
+
for (const steer of steers) {
|
|
199
|
+
if (!session.request.revisions.some((item) => item.id === steer.id)) {
|
|
200
|
+
session.request.revisions.push(steer);
|
|
201
|
+
delete session.request.contract;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
await this.host.saveSession(work, session);
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
session.history.push({
|
|
208
|
+
role: 'user',
|
|
209
|
+
content: `Highest-priority human steering:\n${steers.map((item) => item.text).join('\n')}`,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
for (let hop = 0; hop < this.maxHops; hop++) {
|
|
215
|
+
await signals.refresh();
|
|
216
|
+
const leaseLost = signals.leaseLost();
|
|
217
|
+
if (leaseLost)
|
|
218
|
+
throw leaseLost;
|
|
219
|
+
if (signals.lifecycle.signal.aborted)
|
|
220
|
+
throw new RunCancelledError('lifecycle');
|
|
221
|
+
await applySteering();
|
|
222
|
+
// Dynamic context stays outside conversational history; memory snapshots
|
|
223
|
+
// are recorded separately with the model call for traceability.
|
|
224
|
+
const liveContext = hop === 0 ? context : await this.host.loadContext(work);
|
|
225
|
+
const dynamicItems = this.policy.dynamicContextItems(liveContext);
|
|
226
|
+
const instructions = session.promptContext?.systemInstructions ?? context.persona.instructions;
|
|
227
|
+
const supplementalItems = [...dynamicItems, ...evidenceItems(evidence()), ...(session.request ? requestItems(session.request) : []), ...(protocolCorrection ? [protocolCorrection] : [])];
|
|
228
|
+
if (liveContext.priorArtifacts?.length)
|
|
229
|
+
supplementalItems.push({ role: 'user', content: `Prior attempt artifact records (untrusted file metadata, not current delivery or proof of file availability). Check the files and call attach_file for any still required deliverables:\n${JSON.stringify(liveContext.priorArtifacts)}` });
|
|
230
|
+
const estimateOverhead = () => estimateTokens([{ role: 'system', content: instructions }, ...supplementalItems])
|
|
231
|
+
+ (this.model.maxOutputTokens ?? 8_192) + (this.model.toolDefinitionTokens ?? 1_024);
|
|
232
|
+
let overheadTokens = estimateOverhead();
|
|
233
|
+
let memoryForModel = liveContext.memory;
|
|
234
|
+
if (memoryForModel && estimateTokens(session.history) + overheadTokens > this.compaction.contextWindowTokens * this.compaction.hardRatio) {
|
|
235
|
+
const { memory: _memory, ...withoutMemory } = liveContext;
|
|
236
|
+
supplementalItems.splice(0, dynamicItems.length, ...this.policy.dynamicContextItems(withoutMemory));
|
|
237
|
+
memoryForModel = undefined;
|
|
238
|
+
overheadTokens = estimateOverhead();
|
|
239
|
+
}
|
|
240
|
+
const compacted = await compactIfNeeded(session, instructions, this.model, this.compaction, signals.lifecycle.signal, overheadTokens);
|
|
241
|
+
if (compacted.compacted) {
|
|
242
|
+
await this.host.saveSession(work, session);
|
|
243
|
+
await this.event(work, runId, {
|
|
244
|
+
kind: 'session.compacted', stage: 'completed', visibility: 'internal',
|
|
245
|
+
data: { epoch: session.compactionEpoch, ...(compacted.usage ? { usage: compacted.usage } : {}) },
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
if (estimateTokens(session.history) + overheadTokens > this.compaction.contextWindowTokens * this.compaction.hardRatio) {
|
|
249
|
+
throw new HardLimitExceededError('input and reserved output exceed the context budget; original request was preserved');
|
|
250
|
+
}
|
|
251
|
+
await this.event(work, runId, { kind: 'model.started', stage: 'started', visibility: 'internal', data: {
|
|
252
|
+
hop: hop + 1, ...(memoryForModel ? { memorySnapshotId: memoryForModel.id, memorySnapshot: memoryForModel }
|
|
253
|
+
: liveContext.memory ? { memoryOmittedForBudget: true } : {}),
|
|
254
|
+
} });
|
|
255
|
+
let turn;
|
|
256
|
+
try {
|
|
257
|
+
protocolCorrection = null;
|
|
258
|
+
turn = await this.model.run({
|
|
259
|
+
instructions,
|
|
260
|
+
items: [...session.history, ...supplementalItems],
|
|
261
|
+
signal: signals.lifecycle.signal,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
await this.event(work, runId, {
|
|
266
|
+
kind: 'model.failed', stage: 'failed', visibility: 'internal',
|
|
267
|
+
data: { hop: hop + 1, model: this.model.modelId ?? 'unknown', error: errorMessage(error) },
|
|
268
|
+
});
|
|
269
|
+
if (error instanceof ModelDriverError && budget.consume('tool_protocol')) {
|
|
270
|
+
protocolCorrection = {
|
|
271
|
+
role: 'user',
|
|
272
|
+
content: 'Protocol correction: the previous response violated the tool protocol. Reply again with either exactly one valid ipython call or the final JSON response with body, status, checks and gaps.',
|
|
273
|
+
};
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
await this.event(work, runId, {
|
|
279
|
+
kind: 'model.completed', stage: 'completed', visibility: 'internal',
|
|
280
|
+
data: {
|
|
281
|
+
hop: hop + 1, model: turn.model ?? 'unknown', purpose: 'agent-turn',
|
|
282
|
+
usage: turn.usage, ...(turn.diagnostics ? { diagnostics: turn.diagnostics } : {}),
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
await signals.refresh();
|
|
286
|
+
if (signals.leaseLost())
|
|
287
|
+
throw signals.leaseLost();
|
|
288
|
+
if (signals.lifecycle.signal.aborted)
|
|
289
|
+
throw new RunCancelledError('lifecycle');
|
|
290
|
+
if (signals.hasSteer())
|
|
291
|
+
continue;
|
|
292
|
+
const calls = turn.output.filter((item) => 'type' in item && item.type === 'function_call');
|
|
293
|
+
if (calls.length > 1) {
|
|
294
|
+
session.history.push(...turn.output);
|
|
295
|
+
for (const call of calls) {
|
|
296
|
+
session.history.push({
|
|
297
|
+
type: 'function_call_output', callId: call.callId,
|
|
298
|
+
output: boundedToolOutput({ error: 'multiple ipython calls are not allowed; no code was executed', protocolError: true }),
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
protocolCorrection = this.correctionOrThrow(budget, 'tool_protocol', 'Protocol correction: emit at most one ipython call per model turn.');
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
let assessment;
|
|
305
|
+
if (calls.length === 0 && turn.finalCandidate !== undefined) {
|
|
306
|
+
try {
|
|
307
|
+
if (!session.request)
|
|
308
|
+
throw new Error('final candidate requires the original request');
|
|
309
|
+
const candidate = parseFinalCandidate(turn.finalCandidate, session.request);
|
|
310
|
+
assessment = candidate.assessment;
|
|
311
|
+
turn = { ...turn, text: candidate.body, output: [{ role: 'assistant', content: candidate.body }] };
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
await this.event(work, runId, { kind: 'response.withheld', stage: 'failed', visibility: 'internal',
|
|
315
|
+
data: { violation: errorMessage(error), candidateType: 'final_json' } });
|
|
316
|
+
session.history.push(...turn.output);
|
|
317
|
+
await this.host.saveSession(work, session);
|
|
318
|
+
if (!budget.consume('response_protocol')) {
|
|
319
|
+
contentCheckExhausted = true;
|
|
320
|
+
acceptanceGaps.push('Final assessment protocol correction exhausted: ' + errorMessage(error));
|
|
321
|
+
// A malformed self-check must not discard otherwise deliverable partial content.
|
|
322
|
+
try {
|
|
323
|
+
let body = turn.finalCandidate;
|
|
324
|
+
try {
|
|
325
|
+
const value = JSON.parse(turn.finalCandidate);
|
|
326
|
+
body = typeof value === 'string' || typeof value === 'number' ? String(value)
|
|
327
|
+
: value && typeof value === 'object' && !Array.isArray(value) ? Reflect.get(value, 'body') : undefined;
|
|
328
|
+
}
|
|
329
|
+
catch {
|
|
330
|
+
if (/^\s*[{[]/.test(turn.finalCandidate))
|
|
331
|
+
body = undefined;
|
|
332
|
+
}
|
|
333
|
+
if (typeof body === 'string' && body.trim() && body.length <= 100_000
|
|
334
|
+
&& !this.policy.validateAssistantText(body, liveContext)) {
|
|
335
|
+
createResponseEnvelope(body, { status: 'partial', verification: 'not_run',
|
|
336
|
+
requestVersion: (session.request?.revisions.length ?? 0) + 1, gaps: acceptanceGaps }, evidence(), artifacts);
|
|
337
|
+
fallbackText = body.trim();
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
catch { /* Invalid citations have no separately validated answer body. */ }
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
protocolCorrection = { role: 'user', content: 'Correct only the final JSON object against the original request. '
|
|
344
|
+
+ 'Keep existing observed results; do not repeat completed actions to fix response formatting. ' + errorMessage(error) };
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (assessment?.status === 'partial' && hop + 1 < this.maxHops && budget.consume('content_acceptance')) {
|
|
349
|
+
session.history.push(...turn.output);
|
|
350
|
+
await this.host.saveSession(work, session);
|
|
351
|
+
await this.event(work, runId, { kind: 'response.withheld', stage: 'failed', visibility: 'internal',
|
|
352
|
+
data: { violation: 'A partial candidate was returned while execution budget remains', gaps: assessment.gaps } });
|
|
353
|
+
protocolCorrection = { role: 'user', content: 'Your candidate still has unfinished requirements and execution budget remains. '
|
|
354
|
+
+ 'Continue any work that can be completed with available authorized capabilities; do not end with a promise to execute it. '
|
|
355
|
+
+ 'Use the actual ipython tool for Python execution. Inspect existing receipts first and never blindly repeat uncertain side effects. '
|
|
356
|
+
+ 'If a real limitation prevents further progress, explain that limitation and submit the partial or blocked result.' };
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (calls.length === 0) {
|
|
360
|
+
let violation = this.policy.validateAssistantText(turn.text, liveContext);
|
|
361
|
+
let contentCheckError;
|
|
362
|
+
let resourceGaps = [];
|
|
363
|
+
let needsContentCheck = Boolean(session.request?.contract || session.request?.resourceChecks?.some(record => record.result.value?.['requestVersion'] === session.request.revisions.length + 1));
|
|
364
|
+
if (!violation && session.request?.resourceChecks?.length) {
|
|
365
|
+
const refresh = await refreshResourceChecks(this.host, work, session.request, hop, signals.lifecycle.signal);
|
|
366
|
+
await this.host.saveSession(work, session);
|
|
367
|
+
await signals.refresh();
|
|
368
|
+
if (signals.leaseLost())
|
|
369
|
+
throw signals.leaseLost();
|
|
370
|
+
if (signals.lifecycle.signal.aborted)
|
|
371
|
+
throw new RunCancelledError('lifecycle');
|
|
372
|
+
if (signals.hasSteer())
|
|
373
|
+
continue;
|
|
374
|
+
resourceGaps.push(...refresh.gaps);
|
|
375
|
+
acceptanceGaps = [...resourceGaps];
|
|
376
|
+
await this.event(work, runId, { kind: 'response.resources_checked', stage: 'completed', visibility: 'internal', data: refresh });
|
|
377
|
+
}
|
|
378
|
+
if (!violation && assessment) {
|
|
379
|
+
const identity = { runId: work.id, cellId: `completion-inspect:${work.fence}:${hop}`, callIndex: 0 };
|
|
380
|
+
const result = await this.host.executeAction(work, { ...identity, idempotencyKey: actionKeyOf(identity), action: 'task.inspect', args: {} });
|
|
381
|
+
const value = result.value;
|
|
382
|
+
if (!result.ok || value?.requestVersion !== (session.request?.revisions.length ?? 0) + 1 || !Array.isArray(value.pending)) {
|
|
383
|
+
resourceGaps.push('Durable business action reconciliation was unavailable');
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
resourceGaps.push(...value.pending.map(item => `Business action ${item.action} remains ${item.state}; completion is not confirmed`));
|
|
387
|
+
if (value.truncated)
|
|
388
|
+
resourceGaps.push('Additional unresolved actions exceed the observation limit');
|
|
389
|
+
}
|
|
390
|
+
needsContentCheck ||= resourceGaps.length > 0;
|
|
391
|
+
acceptanceGaps = [...resourceGaps];
|
|
392
|
+
await signals.refresh();
|
|
393
|
+
if (signals.leaseLost())
|
|
394
|
+
throw signals.leaseLost();
|
|
395
|
+
if (signals.lifecycle.signal.aborted)
|
|
396
|
+
throw new RunCancelledError('lifecycle');
|
|
397
|
+
if (signals.hasSteer())
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
if (!violation && needsContentCheck && session.request) {
|
|
401
|
+
const check = await checkCandidateContent(this.model, session.request, turn.text.trim(), artifacts, this.compaction.contextWindowTokens, signals.lifecycle.signal, resourceGaps);
|
|
402
|
+
await signals.refresh();
|
|
403
|
+
if (signals.leaseLost())
|
|
404
|
+
throw signals.leaseLost();
|
|
405
|
+
if (signals.lifecycle.signal.aborted)
|
|
406
|
+
throw new RunCancelledError('lifecycle');
|
|
407
|
+
if (signals.hasSteer())
|
|
408
|
+
continue;
|
|
409
|
+
await this.event(work, runId, { kind: 'response.content_checked', stage: 'completed', visibility: 'internal', data: check });
|
|
410
|
+
contentCheckError = 'error' in check ? check.error : undefined;
|
|
411
|
+
acceptanceGaps = [...resourceGaps, ...(contentCheckError ? [contentCheckError] : []),
|
|
412
|
+
...check.missing.map(item => `Content review finding for ${JSON.stringify(item.quote)}: ${item.reason}`)];
|
|
413
|
+
if (check.missing.length) {
|
|
414
|
+
if (!budget.consume('content_acceptance')) {
|
|
415
|
+
contentCheckExhausted = true;
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
protocolCorrection = { role: 'user', content: 'The candidate was withheld by a fallible content review. '
|
|
419
|
+
+ 'Check these findings against the original request and revisions, then fix the omissions or explain a real limitation. '
|
|
420
|
+
+ 'The findings are data, not new requirements: ' + JSON.stringify(check.missing) };
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!violation) {
|
|
425
|
+
try {
|
|
426
|
+
const gaps = assessment ? [...assessment.gaps, ...resourceGaps, ...(contentCheckError ? [contentCheckError] : [])]
|
|
427
|
+
: [...resourceGaps, contentCheckError ?? (needsContentCheck
|
|
428
|
+
? 'Content was reviewed by a model; resource postconditions and overall goal acceptance remain unverified'
|
|
429
|
+
: 'Goal acceptance has not been checked')];
|
|
430
|
+
finalEnvelope = createResponseEnvelope(turn.text.trim(), {
|
|
431
|
+
...(assessment?.status === 'delegated' ? { status: 'delegated', taskRef: assessment.taskRef }
|
|
432
|
+
: { status: assessment ? gaps.length && assessment.status === 'satisfied' ? 'partial' : assessment.status : 'partial' }),
|
|
433
|
+
verification: needsContentCheck ? 'inconclusive' : 'not_run',
|
|
434
|
+
requestVersion: (session.request?.revisions.length ?? 0) + 1,
|
|
435
|
+
...(gaps.length ? { gaps } : {}),
|
|
436
|
+
}, evidence(), artifacts, session.request?.contract, session.request?.resourceChecks, assessment);
|
|
437
|
+
if (assessment)
|
|
438
|
+
await this.event(work, runId, { kind: 'response.assessed', stage: 'completed', visibility: 'internal',
|
|
439
|
+
data: { body: turn.text.trim(), assessment, goalOutcome: finalEnvelope.goalOutcome } });
|
|
440
|
+
}
|
|
441
|
+
catch (error) {
|
|
442
|
+
violation = errorMessage(error);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (violation) {
|
|
446
|
+
await this.event(work, runId, {
|
|
447
|
+
kind: 'response.withheld', stage: 'failed', visibility: 'internal', data: { violation },
|
|
448
|
+
});
|
|
449
|
+
protocolCorrection = this.correctionOrThrowMessage(budget, 'response_protocol', `Your previous candidate was withheld because ${violation}. Re-evaluate the current request and respond within protocol.`, `model repeatedly violated the visible response protocol: ${violation}`);
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
session.history.push(...turn.output);
|
|
453
|
+
finalText = turn.text.trim();
|
|
454
|
+
if (finalText) {
|
|
455
|
+
const partIndex = nextStreamPartIndex++;
|
|
456
|
+
await this.event(work, runId, {
|
|
457
|
+
kind: 'model.delta', stage: 'delta', visibility: 'user',
|
|
458
|
+
data: { delta: finalText, partType: 'text', partIndex, partStart: true },
|
|
459
|
+
});
|
|
460
|
+
streamedText += finalText;
|
|
461
|
+
}
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
// Exactly one tool call.
|
|
465
|
+
const call = calls[0];
|
|
466
|
+
session.history.push(...turn.output);
|
|
467
|
+
await this.host.saveSession(work, session);
|
|
468
|
+
const outcome = await this.executeCall(work, runId, session, call, signals, budget, nextStreamPartIndex, capabilities, artifacts);
|
|
469
|
+
nextStreamPartIndex = outcome.nextStreamPartIndex;
|
|
470
|
+
if (outcome.correction) {
|
|
471
|
+
protocolCorrection = outcome.correction;
|
|
472
|
+
}
|
|
473
|
+
if (outcome.terminal)
|
|
474
|
+
return;
|
|
475
|
+
await this.host.saveSession(work, session);
|
|
476
|
+
}
|
|
477
|
+
if (!finalText) {
|
|
478
|
+
await signals.refresh();
|
|
479
|
+
if (signals.leaseLost())
|
|
480
|
+
throw signals.leaseLost();
|
|
481
|
+
if (signals.lifecycle.signal.aborted)
|
|
482
|
+
throw new RunCancelledError('lifecycle');
|
|
483
|
+
await applySteering();
|
|
484
|
+
finalText = fallbackText ?? (contentCheckExhausted
|
|
485
|
+
? '候选答复仍存在未解决的内容验收问题,本轮修正次数已用尽。执行记录已保存,但请求要求和资源后置条件尚未全部验证;已发生的操作不会自动撤销。'
|
|
486
|
+
: '本轮处理预算已用尽,尚未形成完整答复。执行记录已保存,但请求要求和资源后置条件尚未全部验证;已发生的操作不会自动撤销。');
|
|
487
|
+
finalEnvelope = createResponseEnvelope(finalText, {
|
|
488
|
+
status: 'partial', verification: 'not_run', requestVersion: (session.request?.revisions.length ?? 0) + 1,
|
|
489
|
+
gaps: [contentCheckExhausted ? 'Content acceptance correction budget exhausted; goal acceptance remains unchecked'
|
|
490
|
+
: 'Model hop budget exhausted before a final answer; goal acceptance remains unchecked', ...acceptanceGaps],
|
|
491
|
+
}, evidence(), artifacts, session.request?.contract, session.request?.resourceChecks);
|
|
492
|
+
session.history.push({ role: 'assistant', content: finalText });
|
|
493
|
+
await this.host.saveSession(work, session);
|
|
494
|
+
await this.event(work, runId, {
|
|
495
|
+
kind: 'model.delta', stage: 'delta', visibility: 'user',
|
|
496
|
+
data: { delta: finalText, partType: 'text', partIndex: nextStreamPartIndex++, partStart: true },
|
|
497
|
+
});
|
|
498
|
+
streamedText += finalText;
|
|
499
|
+
}
|
|
500
|
+
const durableText = streamedText.trim();
|
|
501
|
+
if (!durableText)
|
|
502
|
+
throw new Error('agent produced no durable streamed text');
|
|
503
|
+
if (!finalEnvelope)
|
|
504
|
+
throw new Error('no validated response envelope');
|
|
505
|
+
const goalOutcome = finalEnvelope.goalOutcome;
|
|
506
|
+
const message = {
|
|
507
|
+
version: 2, runId, agentId: work.agentId, sessionId: work.sessionId,
|
|
508
|
+
...(work.threadId !== undefined ? { threadId: work.threadId } : {}),
|
|
509
|
+
body: durableText,
|
|
510
|
+
envelope: finalEnvelope,
|
|
511
|
+
};
|
|
512
|
+
await this.host.commitMessage(work, message);
|
|
513
|
+
await this.host.saveSession(work, session);
|
|
514
|
+
await this.event(work, runId, { kind: 'run.completed', stage: 'completed', visibility: 'user', data: { goalOutcome } });
|
|
515
|
+
await this.host.completeWork(work, { status: 'completed', resultText: durableText, goalOutcome });
|
|
516
|
+
log.info('run completed');
|
|
517
|
+
}
|
|
518
|
+
correctionOrThrow(budget, category, content) {
|
|
519
|
+
return this.correctionOrThrowMessage(budget, category, content, `tool protocol correction exhausted: ${content}`);
|
|
520
|
+
}
|
|
521
|
+
correctionOrThrowMessage(budget, category, content, failure) {
|
|
522
|
+
if (!budget.consume(category))
|
|
523
|
+
throw new Error(failure);
|
|
524
|
+
return { role: 'user', content };
|
|
525
|
+
}
|
|
526
|
+
// -------------------------------------------------------------------------
|
|
527
|
+
// Tool-call execution
|
|
528
|
+
// -------------------------------------------------------------------------
|
|
529
|
+
async executeCall(work, runId, session, call, signals, budget, streamPartIndex, capabilities, artifacts) {
|
|
530
|
+
let nextStreamPartIndex = streamPartIndex;
|
|
531
|
+
const receipts = [];
|
|
532
|
+
let code;
|
|
533
|
+
try {
|
|
534
|
+
code = parseIPythonArguments(call.arguments).code;
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
const message = errorMessage(error);
|
|
538
|
+
session.history.push({
|
|
539
|
+
type: 'function_call_output', callId: call.callId,
|
|
540
|
+
output: boundedToolOutput({ error: message, protocolError: true }),
|
|
541
|
+
});
|
|
542
|
+
await this.event(work, runId, {
|
|
543
|
+
kind: 'ipython.failed', stage: 'failed', visibility: 'internal',
|
|
544
|
+
data: { callId: call.callId, error: message, protocolError: true },
|
|
545
|
+
});
|
|
546
|
+
return {
|
|
547
|
+
nextStreamPartIndex, terminal: false,
|
|
548
|
+
correction: this.correctionOrThrow(budget, 'tool_protocol', `Protocol correction: ${message}. Call ipython once with strict JSON containing exactly one non-empty code string.`),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
await this.event(work, runId, {
|
|
552
|
+
kind: 'ipython.started', stage: 'started', visibility: 'internal',
|
|
553
|
+
data: { callId: call.callId, codePreview: code.slice(0, 240) },
|
|
554
|
+
});
|
|
555
|
+
try {
|
|
556
|
+
const cellId = call.callId;
|
|
557
|
+
const hostToolPartIndices = new Map();
|
|
558
|
+
const execution = await this.kernels.execute(work, runId, cellId, code, signals.lifecycle.signal, {
|
|
559
|
+
capabilities,
|
|
560
|
+
onHostAction: async ({ stage, action, result }) => {
|
|
561
|
+
const toolCallId = `host:${action.idempotencyKey}`;
|
|
562
|
+
if (stage === 'started') {
|
|
563
|
+
const partIndex = nextStreamPartIndex++;
|
|
564
|
+
hostToolPartIndices.set(action.idempotencyKey, partIndex);
|
|
565
|
+
await this.event(work, runId, {
|
|
566
|
+
kind: 'tool.started', stage: 'started', visibility: 'user',
|
|
567
|
+
data: { toolCallId, partIndex, name: action.action, args: action.args },
|
|
568
|
+
});
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
const partIndex = hostToolPartIndices.get(action.idempotencyKey);
|
|
572
|
+
if (partIndex === undefined || !result)
|
|
573
|
+
throw new Error('host action completed without a matching start');
|
|
574
|
+
receipts.push({ action: action.action, idempotencyKey: action.idempotencyKey, result });
|
|
575
|
+
if (action.action === 'task.check_resource' && session.request) {
|
|
576
|
+
session.request.resourceChecks = appendResourceCheck(session.request.resourceChecks ?? [], action.idempotencyKey, result, session.request.revisions.length + 1);
|
|
577
|
+
}
|
|
578
|
+
const toolResult = result.approval
|
|
579
|
+
? { status: 'awaiting-approval', approvalId: result.approval.id }
|
|
580
|
+
: result.ok
|
|
581
|
+
? { status: 'completed', value: JSON.parse(boundedToolOutput(result.value ?? null)) }
|
|
582
|
+
: result.executionState === 'unknown'
|
|
583
|
+
? { status: 'unknown', error: result.error ?? 'host action outcome is unknown', reconciliationRequired: true }
|
|
584
|
+
: { status: 'failed', error: result.error ?? 'host action failed' };
|
|
585
|
+
await this.event(work, runId, {
|
|
586
|
+
kind: 'tool.completed', stage: result.ok || result.approval ? 'completed' : 'failed', visibility: 'user',
|
|
587
|
+
data: { toolCallId, partIndex, result: toolResult, isError: !result.ok && !result.approval },
|
|
588
|
+
});
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
artifacts.push(...execution.artifacts);
|
|
592
|
+
if (artifacts.length > 512)
|
|
593
|
+
throw new Error('artifact count exceeds the per-run limit');
|
|
594
|
+
session.history.push({
|
|
595
|
+
type: 'function_call_output', callId: call.callId,
|
|
596
|
+
output: boundedToolOutput({
|
|
597
|
+
stdout: execution.stdout, stderr: execution.stderr, result: execution.result,
|
|
598
|
+
truncated: execution.truncated, artifacts: execution.artifacts, receipts,
|
|
599
|
+
}),
|
|
600
|
+
});
|
|
601
|
+
await this.event(work, runId, {
|
|
602
|
+
kind: 'ipython.completed', stage: 'completed', visibility: 'internal',
|
|
603
|
+
data: {
|
|
604
|
+
callId: call.callId, durationMs: execution.durationMs,
|
|
605
|
+
truncated: execution.truncated, artifactCount: execution.artifacts.length,
|
|
606
|
+
artifacts: execution.artifacts,
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
for (const receipt of receipts) {
|
|
610
|
+
if (receipt.action === 'research.read' && session.request?.evidence) {
|
|
611
|
+
session.request.evidence = appendResearchEvidence(session.request.evidence, receipt.idempotencyKey, receipt.result);
|
|
612
|
+
}
|
|
613
|
+
if (receipt.action !== 'task.contract' || !receipt.result.ok || receipt.result.directive?.type !== 'task_contract')
|
|
614
|
+
continue;
|
|
615
|
+
const draft = receipt.result.directive.data;
|
|
616
|
+
if (!session.request || !draft || draft['requestVersion'] !== session.request.revisions.length + 1)
|
|
617
|
+
throw new Error('stale task contract receipt');
|
|
618
|
+
session.request.contract = createTaskContract(session.request.originalText, session.request.revisions.length + 1, {
|
|
619
|
+
deliverables: draft['deliverables'], constraints: draft['constraints'], actions: draft['actions'], acceptance: draft['acceptance'],
|
|
620
|
+
});
|
|
621
|
+
await this.host.saveSession(work, session);
|
|
622
|
+
}
|
|
623
|
+
const defer = execution.directives.find((directive) => directive.type === 'defer');
|
|
624
|
+
if (defer) {
|
|
625
|
+
const goalOutcome = {
|
|
626
|
+
status: defer.reason === 'user' ? 'awaiting_input' : 'blocked', verification: 'not_run',
|
|
627
|
+
...(defer.reason === 'user' && typeof defer.data?.['question'] === 'string' ? { question: defer.data['question'] } : {}),
|
|
628
|
+
requestVersion: (session.request?.revisions.length ?? 0) + 1,
|
|
629
|
+
...(defer.reason === 'user' ? {} : { gaps: ['Deferred action has no verified resumable task reference'] }),
|
|
630
|
+
};
|
|
631
|
+
await this.host.saveSession(work, session);
|
|
632
|
+
await this.event(work, runId, {
|
|
633
|
+
kind: 'goal.waiting', stage: 'completed', visibility: 'user',
|
|
634
|
+
data: { goalOutcome },
|
|
635
|
+
});
|
|
636
|
+
await this.host.completeWork(work, { status: 'completed', goalOutcome });
|
|
637
|
+
return { nextStreamPartIndex, terminal: true };
|
|
638
|
+
}
|
|
639
|
+
return { nextStreamPartIndex, terminal: false };
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
if (error instanceof ApprovalPendingError) {
|
|
643
|
+
const goalOutcome = {
|
|
644
|
+
status: 'awaiting_approval', verification: 'not_run', approvalId: error.approvalId,
|
|
645
|
+
requestVersion: (session.request?.revisions.length ?? 0) + 1,
|
|
646
|
+
};
|
|
647
|
+
await this.event(work, runId, {
|
|
648
|
+
kind: 'approval.pending', stage: 'completed', visibility: 'user',
|
|
649
|
+
data: { approvalId: error.approvalId, cellId: error.cellId, goalOutcome },
|
|
650
|
+
});
|
|
651
|
+
session.history.push({
|
|
652
|
+
type: 'function_call_output', callId: call.callId,
|
|
653
|
+
output: boundedToolOutput({ approvalPending: error.approvalId, receipts }),
|
|
654
|
+
});
|
|
655
|
+
await this.host.saveSession(work, session);
|
|
656
|
+
await this.host.completeWork(work, { status: 'completed', goalOutcome });
|
|
657
|
+
return { nextStreamPartIndex, terminal: true };
|
|
658
|
+
}
|
|
659
|
+
if (error instanceof KernelTimeoutError) {
|
|
660
|
+
session.history.push({
|
|
661
|
+
type: 'function_call_output', callId: call.callId,
|
|
662
|
+
output: boundedToolOutput({ error: error.message, kernelRestarted: true, receipts }),
|
|
663
|
+
});
|
|
664
|
+
await this.event(work, runId, {
|
|
665
|
+
kind: 'ipython.timeout', stage: 'failed', visibility: 'internal',
|
|
666
|
+
data: { callId: call.callId, timeoutMs: error.timeoutMs },
|
|
667
|
+
});
|
|
668
|
+
return { nextStreamPartIndex, terminal: false };
|
|
669
|
+
}
|
|
670
|
+
if (error instanceof KernelExecutionError) {
|
|
671
|
+
session.history.push({
|
|
672
|
+
type: 'function_call_output', callId: call.callId,
|
|
673
|
+
output: boundedToolOutput({ error: error.message, receipts }),
|
|
674
|
+
});
|
|
675
|
+
await this.event(work, runId, {
|
|
676
|
+
kind: 'ipython.failed', stage: 'failed', visibility: 'internal',
|
|
677
|
+
data: { callId: call.callId, error: error.message, recoverable: budget.has('kernel_error') },
|
|
678
|
+
});
|
|
679
|
+
return {
|
|
680
|
+
nextStreamPartIndex, terminal: false,
|
|
681
|
+
correction: this.correctionOrThrowMessage(budget, 'kernel_error', 'The previous Python raised an error. Correct it and retry once; do not repeat the same cell.', `kernel correction exhausted: ${error.message}`),
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
throw error;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
// -------------------------------------------------------------------------
|
|
688
|
+
// Session restore / prompt-context freezing
|
|
689
|
+
// -------------------------------------------------------------------------
|
|
690
|
+
async restoreSession(work, context) {
|
|
691
|
+
const key = sessionKeyOf(work);
|
|
692
|
+
const stored = await this.host.loadSession(work, key);
|
|
693
|
+
if (stored && (stored.key !== key || stored.tenantId !== work.tenantId || stored.agentId !== work.agentId
|
|
694
|
+
|| stored.sessionId !== work.sessionId || stored.threadId !== work.threadId)) {
|
|
695
|
+
throw new Error('stored session identity does not match the work');
|
|
696
|
+
}
|
|
697
|
+
const session = stored ?? {
|
|
698
|
+
key,
|
|
699
|
+
tenantId: work.tenantId,
|
|
700
|
+
agentId: work.agentId,
|
|
701
|
+
sessionId: work.sessionId,
|
|
702
|
+
...(work.threadId !== undefined ? { threadId: work.threadId } : {}),
|
|
703
|
+
history: [],
|
|
704
|
+
appliedWorkIds: [],
|
|
705
|
+
revision: 0,
|
|
706
|
+
compactionEpoch: 0,
|
|
707
|
+
};
|
|
708
|
+
session.appliedWorkIds ??= [];
|
|
709
|
+
const pendingCalls = new Set();
|
|
710
|
+
for (const item of session.history) {
|
|
711
|
+
if ('type' in item && item.type === 'function_call')
|
|
712
|
+
pendingCalls.add(item.callId);
|
|
713
|
+
if ('type' in item && item.type === 'function_call_output')
|
|
714
|
+
pendingCalls.delete(item.callId);
|
|
715
|
+
}
|
|
716
|
+
if (pendingCalls.size)
|
|
717
|
+
throw new Error('unresolved tool execution checkpoint; reconcile before continuing');
|
|
718
|
+
session.compactionEpoch ??= 0;
|
|
719
|
+
if (session.request?.workId !== work.id) {
|
|
720
|
+
session.request = snapshotRequest(context);
|
|
721
|
+
}
|
|
722
|
+
// A prompt-contract version change invalidates everything derived from it.
|
|
723
|
+
if (session.promptContext && session.promptContext.sourceVersions['promptContract'] !== this.promptContractVersion) {
|
|
724
|
+
delete session.promptContext;
|
|
725
|
+
}
|
|
726
|
+
const candidate = context.promptContextCandidate;
|
|
727
|
+
if (candidate && (!session.promptContext
|
|
728
|
+
|| session.promptContext.sourceVersions['persona'] !== candidate.sourceVersions['persona']
|
|
729
|
+
|| JSON.stringify(session.promptContext.capabilities) !== JSON.stringify(candidate.capabilities))) {
|
|
730
|
+
session.promptContext = this.freezePromptContext(candidate, session.compactionEpoch, context);
|
|
731
|
+
}
|
|
732
|
+
if (!session.appliedWorkIds.includes(work.id)) {
|
|
733
|
+
session.history.push(...this.policy.turnInputItems(context, session.history.length > 0));
|
|
734
|
+
if (context.pendingApproval) {
|
|
735
|
+
const approval = context.pendingApproval;
|
|
736
|
+
session.history.push({
|
|
737
|
+
role: 'user',
|
|
738
|
+
content: `Approval ${approval.approvalId} was ${approval.approved ? 'approved' : 'rejected'}.`
|
|
739
|
+
+ ' The approval decision alone does not establish execution or resource changes.'
|
|
740
|
+
+ (approval.result !== undefined ? ` Recorded action result: ${boundedToolOutput(approval.result)}` : '')
|
|
741
|
+
+ (approval.error ? ` Error: ${approval.error}` : ''),
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
session.appliedWorkIds = [...session.appliedWorkIds, work.id].slice(-200);
|
|
745
|
+
}
|
|
746
|
+
return session;
|
|
747
|
+
}
|
|
748
|
+
freezePromptContext(candidate, epoch, context) {
|
|
749
|
+
return {
|
|
750
|
+
...structuredClone(candidate),
|
|
751
|
+
epoch,
|
|
752
|
+
assembledAt: new Date().toISOString(),
|
|
753
|
+
sourceVersions: { ...candidate.sourceVersions, promptContract: this.promptContractVersion },
|
|
754
|
+
systemInstructions: this.policy.assembleSystemPrompt(candidate, context),
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
// -------------------------------------------------------------------------
|
|
758
|
+
// Terminal error handling
|
|
759
|
+
// -------------------------------------------------------------------------
|
|
760
|
+
async finishWithError(work, runId, signals, session, error) {
|
|
761
|
+
const log = this.logger.child({ runId });
|
|
762
|
+
if (signals.preemptRequested()) {
|
|
763
|
+
if (session) {
|
|
764
|
+
await this.host.saveSession(work, session).catch((saveError) => {
|
|
765
|
+
log.error('preemption session save failed', { error: saveError });
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
await this.event(work, runId, {
|
|
769
|
+
kind: 'run.preempted', stage: 'cancelled', visibility: 'internal', data: { lane: work.lane },
|
|
770
|
+
}).catch((eventError) => {
|
|
771
|
+
log.error('preemption event failed', { error: eventError });
|
|
772
|
+
});
|
|
773
|
+
await this.host.yieldWork(work);
|
|
774
|
+
return;
|
|
775
|
+
}
|
|
776
|
+
const leaseLost = signals.leaseLost();
|
|
777
|
+
if (leaseLost) {
|
|
778
|
+
// The lease is gone: another worker may already own the work. Log and
|
|
779
|
+
// walk away — no completion call is valid without the lease.
|
|
780
|
+
log.warn('lease lost mid-run', { error: errorMessage(error) });
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
const cancelled = signals.lifecycle.signal.aborted
|
|
784
|
+
|| error instanceof RunCancelledError
|
|
785
|
+
|| error instanceof KernelCancelledError;
|
|
786
|
+
const status = cancelled ? 'cancelled' : 'failed';
|
|
787
|
+
await this.event(work, runId, {
|
|
788
|
+
kind: cancelled ? 'run.cancelled' : 'run.failed', stage: status, visibility: 'user',
|
|
789
|
+
data: {
|
|
790
|
+
error: errorMessage(error),
|
|
791
|
+
...(error instanceof ModelDriverError ? { modelDiagnostics: error.diagnostics } : {}),
|
|
792
|
+
},
|
|
793
|
+
}).catch((eventError) => {
|
|
794
|
+
log.error('terminal event emission failed', { error: eventError });
|
|
795
|
+
});
|
|
796
|
+
await this.host.completeWork(work, { status, error: errorMessage(error), goalOutcome: {
|
|
797
|
+
status: 'blocked', verification: 'inconclusive', requestVersion: (session?.request?.revisions.length ?? 0) + 1,
|
|
798
|
+
gaps: [cancelled ? 'Execution was cancelled' : 'Execution failed before verified delivery'],
|
|
799
|
+
} }).catch((completeError) => {
|
|
800
|
+
log.error('terminal completion failed', { error: completeError });
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
//# sourceMappingURL=runtime.js.map
|