@capybara-hq/kernel 0.1.0-rc.24 → 0.1.0-rc.25
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/dist/admin/admin.module.js +2 -0
- package/dist/admin/health.service.d.ts +2 -2
- package/dist/admin/health.service.js +6 -2
- package/dist/agents/agents.module.js +3 -0
- package/dist/agents/agents.service.d.ts +6 -1
- package/dist/agents/agents.service.js +11 -2
- package/dist/catalog/catalog-import.module.js +0 -4
- package/dist/catalog/catalog-scope.service.d.ts +63 -26
- package/dist/catalog/catalog-scope.service.js +146 -71
- package/dist/catalog/catalog.module.js +4 -3
- package/dist/catalog/catalog.service.d.ts +11 -4
- package/dist/catalog/catalog.service.js +21 -5
- package/dist/catalog/cli.d.ts +4 -3
- package/dist/catalog/cli.js +8 -13
- package/dist/catalog/job-ids.d.ts +8 -20
- package/dist/catalog/job-ids.js +11 -16
- package/dist/catalog/marketplaces.service.d.ts +1 -1
- package/dist/catalog/org-plugin-uploads.d.ts +11 -4
- package/dist/catalog/org-plugin-uploads.js +21 -6
- package/dist/catalog/owned-items.d.ts +6 -13
- package/dist/catalog/owned-items.js +11 -29
- package/dist/catalog/owned-plugin-cleanup.d.ts +8 -17
- package/dist/catalog/owned-plugin-cleanup.js +12 -66
- package/dist/catalog/platform-components.d.ts +56 -34
- package/dist/catalog/platform-components.js +68 -78
- package/dist/catalog/plugin-import.d.ts +28 -25
- package/dist/catalog/plugin-import.js +90 -76
- package/dist/catalog/plugin-upgrade.d.ts +4 -4
- package/dist/catalog/plugin-upgrade.js +7 -19
- package/dist/catalog/scope-jobs.d.ts +59 -0
- package/dist/catalog/scope-jobs.js +65 -0
- package/dist/catalog/scope-sync-workers.d.ts +22 -0
- package/dist/catalog/scope-sync-workers.js +58 -2
- package/dist/catalog/scope-sync.d.ts +40 -65
- package/dist/catalog/scope-sync.js +114 -122
- package/dist/channels/inbound.js +2 -2
- package/dist/channels/threads.d.ts +4 -3
- package/dist/channels/threads.js +4 -3
- package/dist/conversations/conversations.controller.js +4 -2
- package/dist/conversations/conversations.service.d.ts +17 -5
- package/dist/conversations/conversations.service.js +17 -5
- package/dist/conversations/events.d.ts +21 -0
- package/dist/conversations/events.js +24 -1
- package/dist/conversations/source.d.ts +10 -0
- package/dist/conversations/source.js +17 -0
- package/dist/conversations/tree.d.ts +10 -0
- package/dist/conversations/tree.js +35 -0
- package/dist/db/kernel-schema.d.ts +9 -14
- package/dist/devices/devices.service.d.ts +11 -8
- package/dist/devices/devices.service.js +16 -23
- package/dist/evals/run-replay.d.ts +13 -2
- package/dist/evals/run-replay.js +28 -1
- package/dist/evals/snapshot.d.ts +7 -1
- package/dist/evals/snapshot.js +2 -1
- package/dist/extensions/hooks.d.ts +22 -3
- package/dist/extensions/hooks.js +15 -1
- package/dist/extensions/lifecycle-hooks.d.ts +99 -0
- package/dist/extensions/lifecycle-hooks.js +273 -0
- package/dist/extensions/org-config.d.ts +13 -1
- package/dist/extensions/org-config.js +19 -2
- package/dist/extensions/permission-view.d.ts +9 -5
- package/dist/extensions/permission-view.js +9 -4
- package/dist/extensions/point-defaults.d.ts +28 -1
- package/dist/extensions/point-defaults.js +44 -13
- package/dist/inbox/inbox.service.d.ts +6 -20
- package/dist/inbox/inbox.service.js +0 -3
- package/dist/inbox/org-notices.d.ts +1 -1
- package/dist/inbox/platform-inbox.service.d.ts +3 -32
- package/dist/inbox/platform-inbox.service.js +5 -54
- package/dist/infra/outbox/registry.d.ts +124 -0
- package/dist/infra/outbox/registry.js +126 -0
- package/dist/infra/queue/queue-names.d.ts +1 -1
- package/dist/infra/queue/queue-names.js +1 -0
- package/dist/orgs/directory-sync.js +1 -1
- package/dist/orgs/members.service.js +1 -1
- package/dist/orgs/offboarding.d.ts +50 -57
- package/dist/orgs/offboarding.js +117 -162
- package/dist/orgs/orgs.module.js +3 -0
- package/dist/orgs/orgs.service.d.ts +11 -5
- package/dist/orgs/orgs.service.js +15 -6
- package/dist/overrides.d.ts +3 -0
- package/dist/overrides.js +2 -0
- package/dist/platform/ops/inbox.ops.js +4 -20
- package/dist/platform/ops/platform.ops.d.ts +1 -0
- package/dist/platform/ops/platform.ops.js +6 -3
- package/dist/roles/worker-role.d.ts +10 -5
- package/dist/roles/worker-role.js +19 -9
- package/dist/roles/worker.module.js +2 -4
- package/dist/runtime/context/build.d.ts +3 -0
- package/dist/runtime/context/build.js +15 -0
- package/dist/runtime/context/plugin-context.d.ts +20 -0
- package/dist/runtime/context/plugin-context.js +30 -0
- package/dist/runtime/langgraph-loop.js +4 -0
- package/dist/runtime/loop.d.ts +8 -0
- package/dist/runtime/maintenance.d.ts +5 -3
- package/dist/runtime/maintenance.js +36 -11
- package/dist/runtime/model-call.d.ts +6 -1
- package/dist/runtime/model-call.js +1 -0
- package/dist/runtime/plugin-context.service.d.ts +69 -0
- package/dist/runtime/plugin-context.service.js +208 -0
- package/dist/runtime/run-control.d.ts +6 -1
- package/dist/runtime/run-control.js +13 -0
- package/dist/runtime/run-queue.d.ts +13 -0
- package/dist/runtime/run-queue.js +63 -0
- package/dist/runtime/run-worker.d.ts +22 -1
- package/dist/runtime/run-worker.js +107 -10
- package/dist/runtime/runtime.module.js +2 -0
- package/dist/runtime/subagents/dispatch.js +1 -0
- package/dist/runtime/subagents/skill-fork.js +3 -0
- package/dist/sandbox/jobs.d.ts +20 -10
- package/dist/sandbox/jobs.js +23 -17
- package/dist/schedules/fire.d.ts +25 -4
- package/dist/schedules/fire.js +87 -13
- package/dist/schedules/job-ids.d.ts +66 -17
- package/dist/schedules/job-ids.js +81 -13
- package/dist/schedules/run-tracker.js +2 -1
- package/dist/schedules/scan.job.d.ts +13 -32
- package/dist/schedules/scan.job.js +35 -144
- package/dist/schedules/scheduled-tasks.service.d.ts +6 -6
- package/dist/schedules/scheduled-tasks.service.js +17 -14
- package/dist/schedules/schedules.module.js +0 -4
- package/dist/skills/changes.controller.d.ts +1 -1
- package/dist/skills/changes.controller.js +1 -1
- package/dist/tools/approvals.service.d.ts +15 -4
- package/dist/tools/approvals.service.js +26 -10
- package/dist/tools/pipeline.d.ts +11 -2
- package/dist/tools/pipeline.js +5 -0
- package/dist/tools/questions.service.d.ts +15 -4
- package/dist/tools/questions.service.js +21 -19
- package/dist/tools/registry.d.ts +8 -1
- package/dist/tools/registry.js +9 -0
- package/package.json +3 -3
- package/src/migrations/0121_drop_platform_migration_runs.sql +12 -0
- package/src/migrations/0122_drop_scheduled_task_runs_rechecked_at.sql +5 -0
- package/src/migrations/0123_pgvector.sql +27 -0
- package/src/migrations/0124_conversation_source.sql +36 -0
- package/dist/catalog/platform-migration.d.ts +0 -115
- package/dist/catalog/platform-migration.js +0 -583
- package/dist/infra/queue/add-job-once.d.ts +0 -14
- package/dist/infra/queue/add-job-once.js +0 -16
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type ConversationSource } from "@capybara-hq/plugin-sdk";
|
|
2
|
+
import { EventLog } from "../conversations/event-log.js";
|
|
3
|
+
import { ExtensionPointDefaults } from "../extensions/point-defaults.js";
|
|
4
|
+
import { ExtensionRegistry } from "../extensions/registry.js";
|
|
5
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
6
|
+
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
7
|
+
import type { KernelLogger } from "../log.js";
|
|
8
|
+
import type { OrgRole } from "../orgs/roles.js";
|
|
9
|
+
/** What a run start tells {@link PluginContextService.record}. */
|
|
10
|
+
export interface PluginContextInput {
|
|
11
|
+
orgId: string;
|
|
12
|
+
conversationId: string;
|
|
13
|
+
runId: string;
|
|
14
|
+
agentId: string;
|
|
15
|
+
/** The run's agent definition name. */
|
|
16
|
+
agentName: string;
|
|
17
|
+
/** The conversation's owner (`conversations.account_id`) and display name. */
|
|
18
|
+
ownerAccountId: string;
|
|
19
|
+
ownerName: string;
|
|
20
|
+
/** The account whose message started the run. */
|
|
21
|
+
triggerAccountId: string;
|
|
22
|
+
source: ConversationSource;
|
|
23
|
+
/** The trigger account's role in the org. */
|
|
24
|
+
role: OrgRole;
|
|
25
|
+
/** The conversation's kind: only a chat run asks the providers. */
|
|
26
|
+
kind: "chat" | "eval" | "subagent";
|
|
27
|
+
/**
|
|
28
|
+
* The keys of the run's resolved tool set, by plugin (resolved with `forAgent` once, and only
|
|
29
|
+
* when some provider is asked).
|
|
30
|
+
*/
|
|
31
|
+
toolKeysByPlugin: () => Promise<Map<string, string[]>>;
|
|
32
|
+
/** Aborts every provider call (with its own time limit). */
|
|
33
|
+
signal: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run start's plugin context (memory plugin spec §6.1): asks every `context@1` provider enabled in
|
|
37
|
+
* the org (valid config) for its text, in parallel and outside any transaction, each under the
|
|
38
|
+
* run's signal and {@link CONTEXT_LIMITS}.timeoutMs; then appends a `plugin_context` event for each
|
|
39
|
+
* plugin whose text differs from the latest one it recorded in the conversation. Fail open: a
|
|
40
|
+
* provider that throws, times out or returns too much is skipped for this run (its earlier text
|
|
41
|
+
* stays in effect) and logged. A provider loaded here that recorded text but no longer serves the
|
|
42
|
+
* org (switched off, its stored org config or its effective config invalid) gets an empty event,
|
|
43
|
+
* so its text stops following summaries; a plugin not loaded on this replica is left alone
|
|
44
|
+
* (another replica may still load it during a rollout, and blanking it here would make its text
|
|
45
|
+
* flap), and so is one whose status could not be read (a transient read error must not flip the
|
|
46
|
+
* model's context). Chat runs only: an
|
|
47
|
+
* eval replay reads the recorded events, and a sub-agent gets no plugin context (spec §5.4).
|
|
48
|
+
*/
|
|
49
|
+
export declare class PluginContextService {
|
|
50
|
+
private readonly db;
|
|
51
|
+
private readonly eventLog;
|
|
52
|
+
readonly txHost: TransactionHost;
|
|
53
|
+
private readonly points;
|
|
54
|
+
private readonly registry;
|
|
55
|
+
private readonly log;
|
|
56
|
+
constructor(db: KernelDb, eventLog: Pick<EventLog, "appendIn" | "notify">, txHost: TransactionHost, points: Pick<ExtensionPointDefaults, "providersFor">, registry: Pick<ExtensionRegistry, "list">, log: KernelLogger);
|
|
57
|
+
/** Asks the providers and records what changed (see the class); notifies after the commit. */
|
|
58
|
+
record(input: PluginContextInput): Promise<void>;
|
|
59
|
+
/** One provider's call, bounded by the run's signal and the time limit. */
|
|
60
|
+
private ask;
|
|
61
|
+
/**
|
|
62
|
+
* One transaction under the conversation row lock (as `RunContextService`): compares each
|
|
63
|
+
* answer's hash with the plugin's latest `plugin_context` and appends the ones that differ (a
|
|
64
|
+
* plugin with no earlier event and no text writes nothing), plus an empty event for each plugin
|
|
65
|
+
* in `off` whose latest text is not empty. Idem key per run and plugin, so a retried run
|
|
66
|
+
* start finds its events or nothing changed. Returns the last seq written.
|
|
67
|
+
*/
|
|
68
|
+
private append;
|
|
69
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
8
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
9
|
+
};
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { CONTEXT_LIMITS, } from "@capybara-hq/plugin-sdk";
|
|
12
|
+
import { Inject, Injectable } from "@nestjs/common";
|
|
13
|
+
import { EventLog } from "../conversations/event-log.js";
|
|
14
|
+
import { ExtensionPointDefaults } from "../extensions/point-defaults.js";
|
|
15
|
+
import { ExtensionRegistry } from "../extensions/registry.js";
|
|
16
|
+
import { KernelDb } from "../infra/database/kernel-db.js";
|
|
17
|
+
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
18
|
+
import { Transactional } from "../infra/database/transactional.decorator.js";
|
|
19
|
+
import { KERNEL_LOGGER } from "../infra/tokens.js";
|
|
20
|
+
import { latestPluginContexts, pluginContextIdemKey } from "./context/plugin-context.js";
|
|
21
|
+
const sha256 = (text) => createHash("sha256").update(text).digest("hex");
|
|
22
|
+
/**
|
|
23
|
+
* Run start's plugin context (memory plugin spec §6.1): asks every `context@1` provider enabled in
|
|
24
|
+
* the org (valid config) for its text, in parallel and outside any transaction, each under the
|
|
25
|
+
* run's signal and {@link CONTEXT_LIMITS}.timeoutMs; then appends a `plugin_context` event for each
|
|
26
|
+
* plugin whose text differs from the latest one it recorded in the conversation. Fail open: a
|
|
27
|
+
* provider that throws, times out or returns too much is skipped for this run (its earlier text
|
|
28
|
+
* stays in effect) and logged. A provider loaded here that recorded text but no longer serves the
|
|
29
|
+
* org (switched off, its stored org config or its effective config invalid) gets an empty event,
|
|
30
|
+
* so its text stops following summaries; a plugin not loaded on this replica is left alone
|
|
31
|
+
* (another replica may still load it during a rollout, and blanking it here would make its text
|
|
32
|
+
* flap), and so is one whose status could not be read (a transient read error must not flip the
|
|
33
|
+
* model's context). Chat runs only: an
|
|
34
|
+
* eval replay reads the recorded events, and a sub-agent gets no plugin context (spec §5.4).
|
|
35
|
+
*/
|
|
36
|
+
let PluginContextService = class PluginContextService {
|
|
37
|
+
db;
|
|
38
|
+
eventLog;
|
|
39
|
+
txHost;
|
|
40
|
+
points;
|
|
41
|
+
registry;
|
|
42
|
+
log;
|
|
43
|
+
constructor(db, eventLog, txHost, points, registry, log) {
|
|
44
|
+
this.db = db;
|
|
45
|
+
this.eventLog = eventLog;
|
|
46
|
+
this.txHost = txHost;
|
|
47
|
+
this.points = points;
|
|
48
|
+
this.registry = registry;
|
|
49
|
+
this.log = log;
|
|
50
|
+
}
|
|
51
|
+
/** Asks the providers and records what changed (see the class); notifies after the commit. */
|
|
52
|
+
async record(input) {
|
|
53
|
+
if (input.kind !== "chat")
|
|
54
|
+
return;
|
|
55
|
+
let found;
|
|
56
|
+
try {
|
|
57
|
+
found = await this.points.providersFor(input.orgId, "context");
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
// fail open: without the org's providers nothing is asked and nothing is switched off
|
|
61
|
+
this.log.warn("context providers could not be listed; plugin context left as recorded", {
|
|
62
|
+
orgId: input.orgId,
|
|
63
|
+
conversationId: input.conversationId,
|
|
64
|
+
runId: input.runId,
|
|
65
|
+
err,
|
|
66
|
+
});
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const { providers } = found;
|
|
70
|
+
const tools = providers.length > 0 ? await input.toolKeysByPlugin() : new Map();
|
|
71
|
+
const answers = await Promise.all(providers.map(async (p) => {
|
|
72
|
+
const answer = await this.ask(p, input, tools.get(p.extension) ?? []);
|
|
73
|
+
if (!answer.ok) {
|
|
74
|
+
this.log.warn("context provider skipped for this run", {
|
|
75
|
+
orgId: input.orgId,
|
|
76
|
+
conversationId: input.conversationId,
|
|
77
|
+
runId: input.runId,
|
|
78
|
+
plugin: p.extension,
|
|
79
|
+
reason: answer.reason,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return [p.extension, answer];
|
|
83
|
+
}));
|
|
84
|
+
const texts = new Map();
|
|
85
|
+
for (const [plugin, answer] of answers)
|
|
86
|
+
if (answer.ok)
|
|
87
|
+
texts.set(plugin, answer.text);
|
|
88
|
+
const serving = new Set(providers.map((p) => p.extension));
|
|
89
|
+
const off = new Set(this.registry
|
|
90
|
+
.list("context")
|
|
91
|
+
.map((e) => e.plugin)
|
|
92
|
+
// a provider whose status could not be read (a transient error) keeps its recorded text
|
|
93
|
+
.filter((plugin) => !serving.has(plugin) && !found.failed.has(plugin)));
|
|
94
|
+
// nothing answered and nothing to switch off: no transaction, no row lock
|
|
95
|
+
if (texts.size === 0 && off.size === 0)
|
|
96
|
+
return;
|
|
97
|
+
const seq = await this.append(input, texts, off);
|
|
98
|
+
if (seq !== null)
|
|
99
|
+
await this.eventLog.notify(input.conversationId, seq);
|
|
100
|
+
}
|
|
101
|
+
/** One provider's call, bounded by the run's signal and the time limit. */
|
|
102
|
+
async ask(p, input, tools) {
|
|
103
|
+
const signal = AbortSignal.any([input.signal, AbortSignal.timeout(CONTEXT_LIMITS.timeoutMs)]);
|
|
104
|
+
let result;
|
|
105
|
+
try {
|
|
106
|
+
result = await untilAborted(Promise.resolve().then(() => p.impl.provide({
|
|
107
|
+
orgId: input.orgId,
|
|
108
|
+
accountId: input.triggerAccountId,
|
|
109
|
+
agentId: input.agentId,
|
|
110
|
+
agentName: input.agentName,
|
|
111
|
+
conversationId: input.conversationId,
|
|
112
|
+
runId: input.runId,
|
|
113
|
+
source: input.source,
|
|
114
|
+
role: input.role,
|
|
115
|
+
ownerAccountId: input.ownerAccountId,
|
|
116
|
+
ownerName: input.ownerName,
|
|
117
|
+
tools,
|
|
118
|
+
config: p.config,
|
|
119
|
+
signal,
|
|
120
|
+
})), signal);
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (signal.aborted) {
|
|
124
|
+
return { ok: false, reason: input.signal.aborted ? "run aborted" : "timed out" };
|
|
125
|
+
}
|
|
126
|
+
return { ok: false, reason: `threw: ${err instanceof Error ? err.message : String(err)}` };
|
|
127
|
+
}
|
|
128
|
+
if (result === null)
|
|
129
|
+
return { ok: true, text: "" };
|
|
130
|
+
const text = result?.text;
|
|
131
|
+
if (typeof text !== "string")
|
|
132
|
+
return { ok: false, reason: "returned no text" };
|
|
133
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
134
|
+
if (bytes > CONTEXT_LIMITS.maxBytes) {
|
|
135
|
+
return { ok: false, reason: `text of ${bytes} bytes is over ${CONTEXT_LIMITS.maxBytes}` };
|
|
136
|
+
}
|
|
137
|
+
return { ok: true, text };
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* One transaction under the conversation row lock (as `RunContextService`): compares each
|
|
141
|
+
* answer's hash with the plugin's latest `plugin_context` and appends the ones that differ (a
|
|
142
|
+
* plugin with no earlier event and no text writes nothing), plus an empty event for each plugin
|
|
143
|
+
* in `off` whose latest text is not empty. Idem key per run and plugin, so a retried run
|
|
144
|
+
* start finds its events or nothing changed. Returns the last seq written.
|
|
145
|
+
*/
|
|
146
|
+
async append(input, texts, off) {
|
|
147
|
+
const t = this.db.db;
|
|
148
|
+
await t
|
|
149
|
+
.selectFrom("conversations")
|
|
150
|
+
.select("id")
|
|
151
|
+
.where("id", "=", input.conversationId)
|
|
152
|
+
.forUpdate()
|
|
153
|
+
.executeTakeFirst();
|
|
154
|
+
const rows = await t
|
|
155
|
+
.selectFrom("conversation_events")
|
|
156
|
+
.select(["seq", "payload"])
|
|
157
|
+
.where("conversation_id", "=", input.conversationId)
|
|
158
|
+
.where("type", "=", "plugin_context")
|
|
159
|
+
.orderBy("seq", "asc")
|
|
160
|
+
.execute();
|
|
161
|
+
const latest = latestPluginContexts(rows.map((r) => ({ type: "plugin_context", seq: Number(r.seq), payload: r.payload })), Number.POSITIVE_INFINITY);
|
|
162
|
+
const next = new Map(texts);
|
|
163
|
+
for (const [plugin, prev] of latest) {
|
|
164
|
+
if (off.has(plugin) && prev.text !== "")
|
|
165
|
+
next.set(plugin, "");
|
|
166
|
+
}
|
|
167
|
+
const changed = [...next]
|
|
168
|
+
.filter(([plugin, text]) => {
|
|
169
|
+
const prev = latest.get(plugin);
|
|
170
|
+
return prev === undefined ? text !== "" : prev.hash !== sha256(text);
|
|
171
|
+
})
|
|
172
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
173
|
+
if (changed.length === 0)
|
|
174
|
+
return null;
|
|
175
|
+
const written = await this.eventLog.appendIn(input.conversationId, changed.map(([plugin, text]) => ({
|
|
176
|
+
type: "plugin_context",
|
|
177
|
+
runId: input.runId,
|
|
178
|
+
idemKey: pluginContextIdemKey(input.runId, plugin),
|
|
179
|
+
payload: { plugin, text, hash: sha256(text) },
|
|
180
|
+
})));
|
|
181
|
+
return Math.max(...written.map((e) => e.seq));
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
__decorate([
|
|
185
|
+
Transactional()
|
|
186
|
+
], PluginContextService.prototype, "append", null);
|
|
187
|
+
PluginContextService = __decorate([
|
|
188
|
+
Injectable(),
|
|
189
|
+
__param(0, Inject(KernelDb)),
|
|
190
|
+
__param(1, Inject(EventLog)),
|
|
191
|
+
__param(2, Inject(TransactionHost)),
|
|
192
|
+
__param(3, Inject(ExtensionPointDefaults)),
|
|
193
|
+
__param(4, Inject(ExtensionRegistry)),
|
|
194
|
+
__param(5, Inject(KERNEL_LOGGER))
|
|
195
|
+
], PluginContextService);
|
|
196
|
+
export { PluginContextService };
|
|
197
|
+
/** `p`, or a rejection once `signal` aborts (a provider that ignores its signal never holds a run). */
|
|
198
|
+
function untilAborted(p, signal) {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const onAbort = () => reject(signal.reason);
|
|
201
|
+
if (signal.aborted) {
|
|
202
|
+
onAbort();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
206
|
+
p.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
207
|
+
});
|
|
208
|
+
}
|
|
@@ -17,7 +17,7 @@ export declare class RunControl {
|
|
|
17
17
|
/** Owes a deleted root's tree stop in the delete's transaction. */
|
|
18
18
|
private readonly outbox;
|
|
19
19
|
private readonly log;
|
|
20
|
-
constructor(queue: Pick<RunQueue, "ensureRun" | "oweEnsureRun" | "abortIfOrphaned" | "wakeHeldTree" | "oweHeldTreeIn">, waits: Pick<Waits, "abortWaiting" | "childFinished" | "stopChildren" | "stopTree">, abort: Pick<RunAbort, "request">,
|
|
20
|
+
constructor(queue: Pick<RunQueue, "ensureRun" | "oweEnsureRun" | "abortIfOrphaned" | "abortQueued" | "wakeHeldTree" | "oweHeldTreeIn">, waits: Pick<Waits, "abortWaiting" | "childFinished" | "stopChildren" | "stopTree">, abort: Pick<RunAbort, "request">,
|
|
21
21
|
/** The resume of a waiting run in a decision's transaction (a reply's skips). */
|
|
22
22
|
resumes: Pick<RunResume, "resumeIn">,
|
|
23
23
|
/** Owes a deleted root's tree stop in the delete's transaction. */
|
|
@@ -61,6 +61,11 @@ export declare class RunControl {
|
|
|
61
61
|
* stops its children (sub-agents spec §12).
|
|
62
62
|
*/
|
|
63
63
|
abortIfOrphaned(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
|
|
64
|
+
/**
|
|
65
|
+
* Ends a run still `queued` in PG ({@link RunQueue.abortQueued}; the caller wrote the stop marker
|
|
66
|
+
* first): true when it did. A stopped sub-agent then reports to its parent at once.
|
|
67
|
+
*/
|
|
68
|
+
abortQueued(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
|
|
64
69
|
/**
|
|
65
70
|
* 运行在等人处理(waiting)时直接中止:取消等待、写 `run_aborted`、删除 checkpoint,返回 true;
|
|
66
71
|
* 运行已不是 waiting(例如刚被续跑)→ false. The stop's transaction owes the IM delivery that
|
|
@@ -110,6 +110,19 @@ let RunControl = class RunControl {
|
|
|
110
110
|
}
|
|
111
111
|
return r;
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Ends a run still `queued` in PG ({@link RunQueue.abortQueued}; the caller wrote the stop marker
|
|
115
|
+
* first): true when it did. A stopped sub-agent then reports to its parent at once.
|
|
116
|
+
*/
|
|
117
|
+
async abortQueued(conversationId, runId, byAccountId) {
|
|
118
|
+
const r = await this.queue.abortQueued(conversationId, runId, byAccountId);
|
|
119
|
+
if (r) {
|
|
120
|
+
await this.waits
|
|
121
|
+
.childFinished(runId)
|
|
122
|
+
.catch((err) => this.log.error("cannot report finished sub-agent run to its parent", { runId, err }));
|
|
123
|
+
}
|
|
124
|
+
return r;
|
|
125
|
+
}
|
|
113
126
|
/**
|
|
114
127
|
* 运行在等人处理(waiting)时直接中止:取消等待、写 `run_aborted`、删除 checkpoint,返回 true;
|
|
115
128
|
* 运行已不是 waiting(例如刚被续跑)→ false. The stop's transaction owes the IM delivery that
|
|
@@ -148,6 +148,19 @@ export declare class RunQueue {
|
|
|
148
148
|
*/
|
|
149
149
|
abortIfOrphaned(conversationId: string, runId: string, byAccountId: string | null, detail?: RunAbortDetail): Promise<boolean>;
|
|
150
150
|
private abortOrphanIn;
|
|
151
|
+
/**
|
|
152
|
+
* Ends a `queued` run in PG before any worker started it (an offboarded member's run, ruling
|
|
153
|
+
* R27): with the conversation lock free, under the conversation row lock, a run still `queued`
|
|
154
|
+
* is finished `aborted` with `run_aborted` (`consumed_seq` at least to its trigger), so a job
|
|
155
|
+
* that runs later finds it finished and starts nothing. True when it did; false when a worker
|
|
156
|
+
* holds the conversation or is in the middle of the run's first start (`run_started` written,
|
|
157
|
+
* `started_at` not yet set; it hears the stop), or the run is no longer queued. A run resumed
|
|
158
|
+
* from `waiting` to `queued` is ended like a new one. The caller writes the stop marker first, so a worker that takes the
|
|
159
|
+
* conversation meanwhile still stops; a worker that read the run before this commit re-checks it
|
|
160
|
+
* under the row locks before its `run_started` and never moves it back to `running`.
|
|
161
|
+
*/
|
|
162
|
+
abortQueued(conversationId: string, runId: string, byAccountId: string | null): Promise<boolean>;
|
|
163
|
+
private abortQueuedIn;
|
|
151
164
|
/** 会话当前未结束的运行(调用方已持有会话行锁)。 */
|
|
152
165
|
private activeRun;
|
|
153
166
|
/**
|
|
@@ -567,6 +567,66 @@ let RunQueue = class RunQueue {
|
|
|
567
567
|
},
|
|
568
568
|
});
|
|
569
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Ends a `queued` run in PG before any worker started it (an offboarded member's run, ruling
|
|
572
|
+
* R27): with the conversation lock free, under the conversation row lock, a run still `queued`
|
|
573
|
+
* is finished `aborted` with `run_aborted` (`consumed_seq` at least to its trigger), so a job
|
|
574
|
+
* that runs later finds it finished and starts nothing. True when it did; false when a worker
|
|
575
|
+
* holds the conversation or is in the middle of the run's first start (`run_started` written,
|
|
576
|
+
* `started_at` not yet set; it hears the stop), or the run is no longer queued. A run resumed
|
|
577
|
+
* from `waiting` to `queued` is ended like a new one. The caller writes the stop marker first, so a worker that takes the
|
|
578
|
+
* conversation meanwhile still stops; a worker that read the run before this commit re-checks it
|
|
579
|
+
* under the row locks before its `run_started` and never moves it back to `running`.
|
|
580
|
+
*/
|
|
581
|
+
async abortQueued(conversationId, runId, byAccountId) {
|
|
582
|
+
if ((await this.lock.holder(conversationId)) !== null)
|
|
583
|
+
return false;
|
|
584
|
+
const toSeq = await this.abortQueuedIn(conversationId, runId, byAccountId);
|
|
585
|
+
if (toSeq === null)
|
|
586
|
+
return false;
|
|
587
|
+
await notifyFinished(this.eventLog, conversationId, toSeq, "aborted");
|
|
588
|
+
await this.dropCheckpoints(runId);
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
async abortQueuedIn(conversationId, runId, byAccountId) {
|
|
592
|
+
const t = this.db;
|
|
593
|
+
const conv = await t
|
|
594
|
+
.selectFrom("conversations")
|
|
595
|
+
.select("id")
|
|
596
|
+
.where("id", "=", conversationId)
|
|
597
|
+
.forUpdate()
|
|
598
|
+
.executeTakeFirst();
|
|
599
|
+
const active = await this.activeRun(t, conversationId);
|
|
600
|
+
if (!conv || !active || active.id !== runId || active.status !== "queued")
|
|
601
|
+
return null;
|
|
602
|
+
// a worker is in the middle of the run's FIRST start (its `run_started` written, `started_at`
|
|
603
|
+
// not set yet; it holds the conversation since): the stop marker ends it. A run resumed from
|
|
604
|
+
// `waiting` back to `queued` has both from its first start and is ended here like a new one:
|
|
605
|
+
// its worker skips `run_started` (it exists) and its conditional `running` update finds the run
|
|
606
|
+
// finished
|
|
607
|
+
const first = await t
|
|
608
|
+
.selectFrom("runs")
|
|
609
|
+
.select("started_at")
|
|
610
|
+
.where("id", "=", runId)
|
|
611
|
+
.executeTakeFirst();
|
|
612
|
+
if (first?.started_at === null) {
|
|
613
|
+
const started = await t
|
|
614
|
+
.selectFrom("conversation_events")
|
|
615
|
+
.select("seq")
|
|
616
|
+
.where("conversation_id", "=", conversationId)
|
|
617
|
+
.where("idem_key", "=", `run_started:${runId}`)
|
|
618
|
+
.executeTakeFirst();
|
|
619
|
+
if (started)
|
|
620
|
+
return null;
|
|
621
|
+
}
|
|
622
|
+
return this.finisher.finishIn({
|
|
623
|
+
conversationId,
|
|
624
|
+
runId,
|
|
625
|
+
status: "aborted",
|
|
626
|
+
consumed: null,
|
|
627
|
+
event: { type: "run_aborted", payload: { byAccountId, reason: "user" } },
|
|
628
|
+
});
|
|
629
|
+
}
|
|
570
630
|
/** 会话当前未结束的运行(调用方已持有会话行锁)。 */
|
|
571
631
|
activeRun(t, conversationId) {
|
|
572
632
|
return t
|
|
@@ -604,6 +664,9 @@ __decorate([
|
|
|
604
664
|
__decorate([
|
|
605
665
|
Transactional({ propagation: "requires_new" })
|
|
606
666
|
], RunQueue.prototype, "abortOrphanIn", null);
|
|
667
|
+
__decorate([
|
|
668
|
+
Transactional({ propagation: "requires_new" })
|
|
669
|
+
], RunQueue.prototype, "abortQueuedIn", null);
|
|
607
670
|
RunQueue = __decorate([
|
|
608
671
|
Injectable(),
|
|
609
672
|
__param(0, Inject(KernelDb)),
|
|
@@ -8,9 +8,11 @@ import { KernelDb } from "../infra/database/kernel-db.js";
|
|
|
8
8
|
import { TransactionHost } from "../infra/database/transaction-host.js";
|
|
9
9
|
import { KernelEvents } from "../infra/events/kernel-events.js";
|
|
10
10
|
import { SkillsCatalogService } from "../skills/catalog.js";
|
|
11
|
+
import { ToolRegistry } from "../tools/registry.js";
|
|
11
12
|
import { RunAbort } from "./abort.js";
|
|
12
13
|
import { KyselyCheckpointSaver } from "./checkpointer.js";
|
|
13
14
|
import { ConversationLock } from "./conversation-lock.js";
|
|
15
|
+
import { PluginContextService } from "./plugin-context.service.js";
|
|
14
16
|
import { ProviderRegistry } from "./providers.js";
|
|
15
17
|
import { type OrgQuota } from "./quota.js";
|
|
16
18
|
import { RunContextService } from "./run-context.service.js";
|
|
@@ -32,6 +34,12 @@ export interface RunHooks {
|
|
|
32
34
|
lockTtlMs?: number;
|
|
33
35
|
/** 任务结束后的每次 ensureRun 尝试之前调用(attempt 从 0 起);抛错即模拟该次尝试失败。 */
|
|
34
36
|
beforeEnsureRun?(conversationId: string, attempt: number): Promise<void>;
|
|
37
|
+
/** Called after a run job read the run's status, before it takes the conversation lock. */
|
|
38
|
+
afterRunRead?(runId: string, conversationId: string): Promise<void>;
|
|
39
|
+
/** Called with the conversation lock held, before a queued run's `run_started` is written. */
|
|
40
|
+
beforeRunStarted?(runId: string, conversationId: string): Promise<void>;
|
|
41
|
+
/** Called with the conversation lock held, before a queued run's status becomes `running`. */
|
|
42
|
+
beforeRunning?(runId: string, conversationId: string): Promise<void>;
|
|
35
43
|
}
|
|
36
44
|
/** 处理函数的返回值:`ensure` 表示任务完成后再调用一次 ensureRun。 */
|
|
37
45
|
export type ProcessResult = "ensure" | undefined;
|
|
@@ -65,6 +73,10 @@ export declare class RunWorker {
|
|
|
65
73
|
private readonly skillsCatalog;
|
|
66
74
|
/** Run start's run context (run context spec §5): who the agent is, the day, the person. */
|
|
67
75
|
private readonly runContext;
|
|
76
|
+
/** Run start's plugin context (memory plugin spec §6.1): the `context@1` providers' text. */
|
|
77
|
+
private readonly pluginContext;
|
|
78
|
+
/** The run's resolved tools, for the plugin context's `tools` (chat runs with providers). */
|
|
79
|
+
private readonly tools;
|
|
68
80
|
private readonly finisher;
|
|
69
81
|
readonly txHost: TransactionHost;
|
|
70
82
|
/** The steps' live drafts this process writes for the runs it executes. */
|
|
@@ -93,7 +105,11 @@ export declare class RunWorker {
|
|
|
93
105
|
/** Run start's skills catalog snapshot (reads the pinned skills' bodies, phase B). */
|
|
94
106
|
skillsCatalog: Pick<SkillsCatalogService, "record">,
|
|
95
107
|
/** Run start's run context (run context spec §5): who the agent is, the day, the person. */
|
|
96
|
-
runContext: Pick<RunContextService, "record">,
|
|
108
|
+
runContext: Pick<RunContextService, "record">,
|
|
109
|
+
/** Run start's plugin context (memory plugin spec §6.1): the `context@1` providers' text. */
|
|
110
|
+
pluginContext: Pick<PluginContextService, "record">,
|
|
111
|
+
/** The run's resolved tools, for the plugin context's `tools` (chat runs with providers). */
|
|
112
|
+
tools: Pick<ToolRegistry, "forAgent">, finisher: Pick<RunFinish, "finish">, txHost: TransactionHost,
|
|
97
113
|
/** The steps' live drafts this process writes for the runs it executes. */
|
|
98
114
|
drafts: RunDrafts, hooks?: RunHooks | undefined);
|
|
99
115
|
private get db();
|
|
@@ -159,6 +175,11 @@ export declare class RunWorker {
|
|
|
159
175
|
process(job: Job<RunJobData>, token: string | undefined): Promise<ProcessResult>;
|
|
160
176
|
private execute;
|
|
161
177
|
private completed;
|
|
178
|
+
/**
|
|
179
|
+
* The run's extension tool keys by plugin, resolved as a chat run's first step lists them
|
|
180
|
+
* (disabled tools, org switches, the conversation's source): a provider's `tools`.
|
|
181
|
+
*/
|
|
182
|
+
private pluginToolKeys;
|
|
162
183
|
/**
|
|
163
184
|
* When the conversation was switched to a model that no longer resolves (provider disabled,
|
|
164
185
|
* model removed), writes `model_switched { model: null, effort: null, reason: "unavailable" }`
|