@things-factory/ai-assistant 10.1.203 → 10.1.205
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-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/assistant-mode/index.d.ts +1 -1
- package/dist-server/service/assistant-mode/index.js.map +1 -1
- package/dist-server/service/assistant-mode/mode-categories.d.ts +10 -0
- package/dist-server/service/assistant-mode/mode-categories.js +10 -0
- package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
- package/dist-server/service/assistant-mode/types.d.ts +14 -0
- package/dist-server/service/assistant-mode/types.js.map +1 -1
- package/dist-server/service/index.d.ts +2 -1
- package/dist-server/service/index.js +4 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/unattended-run/assistant-run.d.ts +45 -0
- package/dist-server/service/unattended-run/assistant-run.js +248 -0
- package/dist-server/service/unattended-run/assistant-run.js.map +1 -0
- package/dist-server/service/unattended-run/index.d.ts +8 -0
- package/dist-server/service/unattended-run/index.js +15 -0
- package/dist-server/service/unattended-run/index.js.map +1 -0
- package/dist-server/service/unattended-run/limits.d.ts +9 -0
- package/dist-server/service/unattended-run/limits.js +24 -0
- package/dist-server/service/unattended-run/limits.js.map +1 -0
- package/dist-server/service/unattended-run/run-as.d.ts +24 -0
- package/dist-server/service/unattended-run/run-as.js +26 -0
- package/dist-server/service/unattended-run/run-as.js.map +1 -0
- package/dist-server/service/unattended-run/run-budget.d.ts +24 -0
- package/dist-server/service/unattended-run/run-budget.js +46 -0
- package/dist-server/service/unattended-run/run-budget.js.map +1 -0
- package/dist-server/service/unattended-run/run-ledger.d.ts +13 -0
- package/dist-server/service/unattended-run/run-ledger.js +29 -0
- package/dist-server/service/unattended-run/run-ledger.js.map +1 -0
- package/dist-server/service/unattended-run/run-outcome.d.ts +9 -0
- package/dist-server/service/unattended-run/run-outcome.js +46 -0
- package/dist-server/service/unattended-run/run-outcome.js.map +1 -0
- package/dist-server/service/unattended-run/run-store.d.ts +31 -0
- package/dist-server/service/unattended-run/run-store.js +108 -0
- package/dist-server/service/unattended-run/run-store.js.map +1 -0
- package/dist-server/service/unattended-run/run-unattended.d.ts +50 -0
- package/dist-server/service/unattended-run/run-unattended.js +214 -0
- package/dist-server/service/unattended-run/run-unattended.js.map +1 -0
- package/dist-server/service/unattended-run/unattended-prompt.d.ts +3 -0
- package/dist-server/service/unattended-run/unattended-prompt.js +27 -0
- package/dist-server/service/unattended-run/unattended-prompt.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/server/service/assistant-mode/index.ts +1 -1
- package/server/service/assistant-mode/mode-categories.ts +10 -0
- package/server/service/assistant-mode/types.ts +15 -0
- package/server/service/index.ts +4 -1
- package/server/service/unattended-run/assistant-run.ts +199 -0
- package/server/service/unattended-run/index.ts +10 -0
- package/server/service/unattended-run/limits.ts +24 -0
- package/server/service/unattended-run/run-as.ts +38 -0
- package/server/service/unattended-run/run-budget.ts +58 -0
- package/server/service/unattended-run/run-ledger.ts +31 -0
- package/server/service/unattended-run/run-outcome.ts +43 -0
- package/server/service/unattended-run/run-store-race.pg.test.ts +83 -0
- package/server/service/unattended-run/run-store.ts +162 -0
- package/server/service/unattended-run/run-unattended.test.ts +477 -0
- package/server/service/unattended-run/run-unattended.ts +246 -0
- package/server/service/unattended-run/unattended-prompt.ts +24 -0
- package/translations/en.json +15 -1
- package/translations/es.json +15 -1
- package/translations/ja.json +15 -1
- package/translations/ko.json +15 -1
- package/translations/ms.json +15 -1
- package/translations/zh.json +15 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Who the run is - judged again at every run (design/ai/unattended-run.md §2, ADR-0142 §8.1).
|
|
4
|
+
*
|
|
5
|
+
* The run acts for the policy owner and for no one else. There is no second user to fall back to: a refused owner
|
|
6
|
+
* means a refused run, never one under an administrator.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.ASK_ASSISTANT_GATE = void 0;
|
|
10
|
+
exports.judgeRunAs = judgeRunAs;
|
|
11
|
+
/** The same door a person passes to ask the assistant (`assistantChat`). */
|
|
12
|
+
exports.ASK_ASSISTANT_GATE = { category: 'ai-assistant', privilege: 'mutation', owner: true };
|
|
13
|
+
/** `user` is the account as `findUser` read it just now - absent when there is none. */
|
|
14
|
+
async function judgeRunAs(user, domain, checks) {
|
|
15
|
+
if (!user)
|
|
16
|
+
return { refused: 'assistant-run-as-unknown' };
|
|
17
|
+
/* Only an account that can sign in now. Locked, banned, deleted, or one waiting for a password reset is not. */
|
|
18
|
+
if (user.status !== 'activated')
|
|
19
|
+
return { refused: 'assistant-run-as-inactive' };
|
|
20
|
+
if (!(await checks.mayCheckin(user, domain)))
|
|
21
|
+
return { refused: 'assistant-run-as-not-member' };
|
|
22
|
+
if (!(await checks.permits(exports.ASK_ASSISTANT_GATE, user, domain)))
|
|
23
|
+
return { refused: 'assistant-run-as-not-permitted' };
|
|
24
|
+
return { user };
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=run-as.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-as.js","sourceRoot":"","sources":["../../../server/service/unattended-run/run-as.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;;AAqBH,gCAWC;AA9BD,4EAA4E;AAC/D,QAAA,kBAAkB,GAAG,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,CAAA;AAiBlG,wFAAwF;AACjF,KAAK,UAAU,UAAU,CAC9B,IAA8B,EAC9B,MAAe,EACf,MAAqD;IAErD,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,0BAA0B,EAAE,CAAA;IACzD,gHAAgH;IAChH,IAAI,IAAI,CAAC,MAAM,KAAK,WAAW;QAAE,OAAO,EAAE,OAAO,EAAE,2BAA2B,EAAE,CAAA;IAChF,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,6BAA6B,EAAE,CAAA;IAC/F,IAAI,CAAC,CAAC,MAAM,MAAM,CAAC,OAAO,CAAC,0BAAkB,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,gCAAgC,EAAE,CAAA;IACnH,OAAO,EAAE,IAAI,EAAE,CAAA;AACjB,CAAC","sourcesContent":["/*\n * Who the run is - judged again at every run (design/ai/unattended-run.md §2, ADR-0142 §8.1).\n *\n * The run acts for the policy owner and for no one else. There is no second user to fall back to: a refused owner\n * means a refused run, never one under an administrator.\n */\n\n/** The same door a person passes to ask the assistant (`assistantChat`). */\nexport const ASK_ASSISTANT_GATE = { category: 'ai-assistant', privilege: 'mutation', owner: true }\n\nexport type RunAsRefusal =\n | 'assistant-run-as-unknown'\n | 'assistant-run-as-inactive'\n | 'assistant-run-as-not-member'\n | 'assistant-run-as-not-permitted'\n\nexport interface RunAsChecks<TUser, TDomain> {\n /** The user as stored now - not as the caller last saw them. */\n findUser: (id: string) => Promise<TUser | undefined | null>\n /** Whether they can enter this domain - auth-base `canUserCheckinDomain`. */\n mayCheckin: (user: TUser, domain: TDomain) => Promise<boolean>\n /** auth-base `checkPermission`, with no address and no prohibitions: there is no request. */\n permits: (gate: typeof ASK_ASSISTANT_GATE, user: TUser, domain: TDomain) => Promise<boolean>\n}\n\n/** `user` is the account as `findUser` read it just now - absent when there is none. */\nexport async function judgeRunAs<TUser extends { id?: string; status?: string }, TDomain>(\n user: TUser | undefined | null,\n domain: TDomain,\n checks: Omit<RunAsChecks<TUser, TDomain>, 'findUser'>\n): Promise<{ user: TUser } | { refused: RunAsRefusal }> {\n if (!user) return { refused: 'assistant-run-as-unknown' }\n /* Only an account that can sign in now. Locked, banned, deleted, or one waiting for a password reset is not. */\n if (user.status !== 'activated') return { refused: 'assistant-run-as-inactive' }\n if (!(await checks.mayCheckin(user, domain))) return { refused: 'assistant-run-as-not-member' }\n if (!(await checks.permits(ASK_ASSISTANT_GATE, user, domain))) return { refused: 'assistant-run-as-not-permitted' }\n return { user }\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { AgentClient } from '@things-factory/ai-client-base';
|
|
2
|
+
export interface RunSpend {
|
|
3
|
+
modelCalls: number;
|
|
4
|
+
inputTokens: number;
|
|
5
|
+
outputTokens: number;
|
|
6
|
+
/** Set once a request was refused, with the limit it hit. */
|
|
7
|
+
exceeded?: {
|
|
8
|
+
limit: 'model-calls' | 'tokens';
|
|
9
|
+
max: number;
|
|
10
|
+
used: number;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export declare class RunBudgetExceeded extends Error {
|
|
14
|
+
readonly params: NonNullable<RunSpend['exceeded']>;
|
|
15
|
+
readonly code = "assistant-run-budget-exceeded";
|
|
16
|
+
constructor(params: NonNullable<RunSpend['exceeded']>);
|
|
17
|
+
}
|
|
18
|
+
export declare function budgetedClient(client: AgentClient, limits: {
|
|
19
|
+
maxModelCalls: number;
|
|
20
|
+
maxTokens: number;
|
|
21
|
+
}): {
|
|
22
|
+
client: AgentClient;
|
|
23
|
+
spend: RunSpend;
|
|
24
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RunBudgetExceeded = void 0;
|
|
4
|
+
exports.budgetedClient = budgetedClient;
|
|
5
|
+
class RunBudgetExceeded extends Error {
|
|
6
|
+
constructor(params) {
|
|
7
|
+
super(`the run reached its ${params.limit} limit (${params.used} of ${params.max})`);
|
|
8
|
+
this.params = params;
|
|
9
|
+
this.code = 'assistant-run-budget-exceeded';
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
exports.RunBudgetExceeded = RunBudgetExceeded;
|
|
13
|
+
function budgetedClient(client, limits) {
|
|
14
|
+
const spend = { modelCalls: 0, inputTokens: 0, outputTokens: 0 };
|
|
15
|
+
const admit = () => {
|
|
16
|
+
if (spend.modelCalls >= limits.maxModelCalls) {
|
|
17
|
+
spend.exceeded ??= { limit: 'model-calls', max: limits.maxModelCalls, used: spend.modelCalls };
|
|
18
|
+
}
|
|
19
|
+
else if (spend.inputTokens + spend.outputTokens >= limits.maxTokens) {
|
|
20
|
+
spend.exceeded ??= { limit: 'tokens', max: limits.maxTokens, used: spend.inputTokens + spend.outputTokens };
|
|
21
|
+
}
|
|
22
|
+
if (spend.exceeded)
|
|
23
|
+
throw new RunBudgetExceeded(spend.exceeded);
|
|
24
|
+
spend.modelCalls++;
|
|
25
|
+
};
|
|
26
|
+
const counted = (options) => ({
|
|
27
|
+
...options,
|
|
28
|
+
onUsage: (usage) => {
|
|
29
|
+
spend.inputTokens += Number(usage?.inputTokens) || 0;
|
|
30
|
+
spend.outputTokens += Number(usage?.outputTokens) || 0;
|
|
31
|
+
options?.onUsage?.(usage);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
const wrapped = {
|
|
35
|
+
id: client.id,
|
|
36
|
+
agent: client.agent,
|
|
37
|
+
chat: (messages, options) => (admit(), client.chat(messages, counted(options))),
|
|
38
|
+
generateJSON: (prompt, options) => (admit(), client.generateJSON(prompt, counted(options))),
|
|
39
|
+
...(client.chatWithTools && {
|
|
40
|
+
chatWithTools: (messages, tools, options) => (admit(), client.chatWithTools(messages, tools, counted(options)))
|
|
41
|
+
}),
|
|
42
|
+
listModels: () => client.listModels()
|
|
43
|
+
};
|
|
44
|
+
return { client: wrapped, spend };
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=run-budget.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-budget.js","sourceRoot":"","sources":["../../../server/service/unattended-run/run-budget.ts"],"names":[],"mappings":";;;AAwBA,wCAiCC;AAxCD,MAAa,iBAAkB,SAAQ,KAAK;IAE1C,YAAqB,MAAyC;QAC5D,KAAK,CAAC,uBAAuB,MAAM,CAAC,KAAK,WAAW,MAAM,CAAC,IAAI,OAAO,MAAM,CAAC,GAAG,GAAG,CAAC,CAAA;QADjE,WAAM,GAAN,MAAM,CAAmC;QADrD,SAAI,GAAG,+BAA+B,CAAA;IAG/C,CAAC;CACF;AALD,8CAKC;AAED,SAAgB,cAAc,CAAC,MAAmB,EAAE,MAAoD;IACtG,MAAM,KAAK,GAAa,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,CAAA;IAE1E,MAAM,KAAK,GAAG,GAAG,EAAE;QACjB,IAAI,KAAK,CAAC,UAAU,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;YAC7C,KAAK,CAAC,QAAQ,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,GAAG,EAAE,MAAM,CAAC,aAAa,EAAE,IAAI,EAAE,KAAK,CAAC,UAAU,EAAE,CAAA;QAChG,CAAC;aAAM,IAAI,KAAK,CAAC,WAAW,GAAG,KAAK,CAAC,YAAY,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC;YACtE,KAAK,CAAC,QAAQ,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,CAAC,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,WAAW,GAAG,KAAK,CAAC,YAAY,EAAE,CAAA;QAC7G,CAAC;QACD,IAAI,KAAK,CAAC,QAAQ;YAAE,MAAM,IAAI,iBAAiB,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAA;QAC/D,KAAK,CAAC,UAAU,EAAE,CAAA;IACpB,CAAC,CAAA;IAED,MAAM,OAAO,GAAG,CAAC,OAAY,EAAE,EAAE,CAAC,CAAC;QACjC,GAAG,OAAO;QACV,OAAO,EAAE,CAAC,KAAU,EAAE,EAAE;YACtB,KAAK,CAAC,WAAW,IAAI,MAAM,CAAC,KAAK,EAAE,WAAW,CAAC,IAAI,CAAC,CAAA;YACpD,KAAK,CAAC,YAAY,IAAI,MAAM,CAAC,KAAK,EAAE,YAAY,CAAC,IAAI,CAAC,CAAA;YACtD,OAAO,EAAE,OAAO,EAAE,CAAC,KAAK,CAAC,CAAA;QAC3B,CAAC;KACF,CAAC,CAAA;IAEF,MAAM,OAAO,GAAgB;QAC3B,EAAE,EAAE,MAAM,CAAC,EAAE;QACb,KAAK,EAAE,MAAM,CAAC,KAAK;QACnB,IAAI,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;QAC/E,YAAY,EAAE,CAAC,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,MAAM,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;QAC3F,GAAG,CAAC,MAAM,CAAC,aAAa,IAAI;YAC1B,aAAa,EAAE,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,EAAE,MAAM,CAAC,aAAc,CAAC,QAAQ,EAAE,KAAK,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC;SACjH,CAAC;QACF,UAAU,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,UAAU,EAAE;KACvB,CAAA;IAChB,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,CAAA;AACnC,CAAC","sourcesContent":["/*\n * The client one unattended run talks through - it counts what the run spends and refuses the request past the limit\n * (design/ai/unattended-run.md §4).\n *\n * It wraps the domain's metered client, so every request still writes its AiUsage row at the provider boundary\n * (ADR-0061 decision 6); this only adds the run's own ceiling.\n */\nimport type { AgentClient } from '@things-factory/ai-client-base'\n\nexport interface RunSpend {\n modelCalls: number\n inputTokens: number\n outputTokens: number\n /** Set once a request was refused, with the limit it hit. */\n exceeded?: { limit: 'model-calls' | 'tokens'; max: number; used: number }\n}\n\nexport class RunBudgetExceeded extends Error {\n readonly code = 'assistant-run-budget-exceeded'\n constructor(readonly params: NonNullable<RunSpend['exceeded']>) {\n super(`the run reached its ${params.limit} limit (${params.used} of ${params.max})`)\n }\n}\n\nexport function budgetedClient(client: AgentClient, limits: { maxModelCalls: number; maxTokens: number }): { client: AgentClient; spend: RunSpend } {\n const spend: RunSpend = { modelCalls: 0, inputTokens: 0, outputTokens: 0 }\n\n const admit = () => {\n if (spend.modelCalls >= limits.maxModelCalls) {\n spend.exceeded ??= { limit: 'model-calls', max: limits.maxModelCalls, used: spend.modelCalls }\n } else if (spend.inputTokens + spend.outputTokens >= limits.maxTokens) {\n spend.exceeded ??= { limit: 'tokens', max: limits.maxTokens, used: spend.inputTokens + spend.outputTokens }\n }\n if (spend.exceeded) throw new RunBudgetExceeded(spend.exceeded)\n spend.modelCalls++\n }\n\n const counted = (options: any) => ({\n ...options,\n onUsage: (usage: any) => {\n spend.inputTokens += Number(usage?.inputTokens) || 0\n spend.outputTokens += Number(usage?.outputTokens) || 0\n options?.onUsage?.(usage)\n }\n })\n\n const wrapped: AgentClient = {\n id: client.id,\n agent: client.agent,\n chat: (messages, options) => (admit(), client.chat(messages, counted(options))),\n generateJSON: (prompt, options) => (admit(), client.generateJSON(prompt, counted(options))),\n ...(client.chatWithTools && {\n chatWithTools: (messages, tools, options) => (admit(), client.chatWithTools!(messages, tools, counted(options)))\n }),\n listModels: () => client.listModels()\n } as AgentClient\n return { client: wrapped, spend }\n}\n"]}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type AssistantRunStatus = 'running' | 'succeeded' | 'failed' | 'refused';
|
|
2
|
+
export interface LedgerRow {
|
|
3
|
+
status: AssistantRunStatus | string;
|
|
4
|
+
deadlineAt?: Date | string | null;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* `return` - hand the stored row back, run nothing. `retake` - start the next attempt on the same row.
|
|
8
|
+
*
|
|
9
|
+
* A succeeded run is the answer. A running one is left alone until its deadline; past it the server that held it is
|
|
10
|
+
* taken as gone. A failed or refused one runs again - a retry is for exactly that, and how many retries there are is
|
|
11
|
+
* the caller's limit (scheduler-client `max_retry_count`), not this one's.
|
|
12
|
+
*/
|
|
13
|
+
export declare function nextStep(row: LedgerRow, now: Date): 'return' | 'retake';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Pure. What a call for an occurrence that already has a row does (design/ai/unattended-run.md §5).
|
|
4
|
+
*
|
|
5
|
+
* One row is one occurrence - (domain, source, subject, scheduledAt). scheduler-client calls again when an answer came
|
|
6
|
+
* late or failed; a second call must not run the model a second time while the first is alive or after it succeeded.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.nextStep = nextStep;
|
|
10
|
+
/**
|
|
11
|
+
* `return` - hand the stored row back, run nothing. `retake` - start the next attempt on the same row.
|
|
12
|
+
*
|
|
13
|
+
* A succeeded run is the answer. A running one is left alone until its deadline; past it the server that held it is
|
|
14
|
+
* taken as gone. A failed or refused one runs again - a retry is for exactly that, and how many retries there are is
|
|
15
|
+
* the caller's limit (scheduler-client `max_retry_count`), not this one's.
|
|
16
|
+
*/
|
|
17
|
+
function nextStep(row, now) {
|
|
18
|
+
if (row.status === 'succeeded')
|
|
19
|
+
return 'return';
|
|
20
|
+
if (row.status === 'running') {
|
|
21
|
+
const deadline = row.deadlineAt ? new Date(row.deadlineAt).valueOf() : NaN;
|
|
22
|
+
/* A running row with no readable deadline is not taken over - two runs at once is the one thing this prevents. */
|
|
23
|
+
if (!Number.isFinite(deadline))
|
|
24
|
+
return 'return';
|
|
25
|
+
return deadline > now.valueOf() ? 'return' : 'retake';
|
|
26
|
+
}
|
|
27
|
+
return 'retake';
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=run-ledger.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-ledger.js","sourceRoot":"","sources":["../../../server/service/unattended-run/run-ledger.ts"],"names":[],"mappings":";AAAA;;;;;GAKG;;AAgBH,4BASC;AAhBD;;;;;;GAMG;AACH,SAAgB,QAAQ,CAAC,GAAc,EAAE,GAAS;IAChD,IAAI,GAAG,CAAC,MAAM,KAAK,WAAW;QAAE,OAAO,QAAQ,CAAA;IAC/C,IAAI,GAAG,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,QAAQ,GAAG,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,GAAG,CAAA;QAC1E,kHAAkH;QAClH,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,OAAO,QAAQ,CAAA;QAC/C,OAAO,QAAQ,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAA;IACvD,CAAC;IACD,OAAO,QAAQ,CAAA;AACjB,CAAC","sourcesContent":["/*\n * Pure. What a call for an occurrence that already has a row does (design/ai/unattended-run.md §5).\n *\n * One row is one occurrence - (domain, source, subject, scheduledAt). scheduler-client calls again when an answer came\n * late or failed; a second call must not run the model a second time while the first is alive or after it succeeded.\n */\n\nexport type AssistantRunStatus = 'running' | 'succeeded' | 'failed' | 'refused'\n\nexport interface LedgerRow {\n status: AssistantRunStatus | string\n deadlineAt?: Date | string | null\n}\n\n/**\n * `return` - hand the stored row back, run nothing. `retake` - start the next attempt on the same row.\n *\n * A succeeded run is the answer. A running one is left alone until its deadline; past it the server that held it is\n * taken as gone. A failed or refused one runs again - a retry is for exactly that, and how many retries there are is\n * the caller's limit (scheduler-client `max_retry_count`), not this one's.\n */\nexport function nextStep(row: LedgerRow, now: Date): 'return' | 'retake' {\n if (row.status === 'succeeded') return 'return'\n if (row.status === 'running') {\n const deadline = row.deadlineAt ? new Date(row.deadlineAt).valueOf() : NaN\n /* A running row with no readable deadline is not taken over - two runs at once is the one thing this prevents. */\n if (!Number.isFinite(deadline)) return 'return'\n return deadline > now.valueOf() ? 'return' : 'retake'\n }\n return 'retake'\n}\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const outcomeKey: (code: string) => string;
|
|
2
|
+
export declare const ASSISTANT_RUN_OUTCOME_ENGLISH: Record<string, string>;
|
|
3
|
+
type Translate = (key: string, options?: Record<string, unknown>) => string;
|
|
4
|
+
/** The sentence for a run's outcome, or undefined when it has none (running, or succeeded). */
|
|
5
|
+
export declare function assistantRunOutcomeText(run: {
|
|
6
|
+
outcomeCode?: string | null;
|
|
7
|
+
outcomeParams?: string | null;
|
|
8
|
+
}, t?: Translate): string | undefined;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*
|
|
3
|
+
* Why a run failed or was refused, as a person reads it (design/ai/unattended-run.md §7).
|
|
4
|
+
*
|
|
5
|
+
* The run keeps a code and its values (ADR-0054); the sentence is made where it is shown, under this package's key
|
|
6
|
+
* `ai-assistant.run.<code>` - an outcome is a stored value, not a thrown refusal. The English below is the sentence
|
|
7
|
+
* when no translation has the key; the application that shows a run does not write its own.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.ASSISTANT_RUN_OUTCOME_ENGLISH = exports.outcomeKey = void 0;
|
|
11
|
+
exports.assistantRunOutcomeText = assistantRunOutcomeText;
|
|
12
|
+
const outcomeKey = (code) => `ai-assistant.run.${code}`;
|
|
13
|
+
exports.outcomeKey = outcomeKey;
|
|
14
|
+
exports.ASSISTANT_RUN_OUTCOME_ENGLISH = {
|
|
15
|
+
'assistant-run-as-unknown': 'the account the run was to act as does not exist.',
|
|
16
|
+
'assistant-run-as-inactive': "the owner's account is not active, so the run did not start.",
|
|
17
|
+
'assistant-run-as-not-member': 'the owner can no longer enter this domain, so the run did not start.',
|
|
18
|
+
'assistant-run-as-not-permitted': 'the owner may no longer ask the AI assistant, so the run did not start.',
|
|
19
|
+
'assistant-mode-not-unattended': 'the assistant mode {kind} does not run without a person.',
|
|
20
|
+
'unknown-assistant-mode': 'this server has no assistant mode {kind}.',
|
|
21
|
+
'assistant-run-budget-exceeded': 'the run reached its {limit} limit ({used} of {max}) and stopped.',
|
|
22
|
+
'assistant-run-step-limit': 'the run did not finish within {steps} steps.',
|
|
23
|
+
'assistant-run-timeout': 'the run did not finish within {timeoutMs} ms.',
|
|
24
|
+
'assistant-run-empty-draft': 'the run ended without writing a draft.',
|
|
25
|
+
'assistant-run-model-error': 'the AI model failed during the run.',
|
|
26
|
+
'assistant-run-abandoned': 'the run stopped partway and was started again.',
|
|
27
|
+
'ai-not-configured': 'AI is not set up for this domain.',
|
|
28
|
+
'ai-agent-not-available': 'the AI agent picked for this run is not available here.'
|
|
29
|
+
};
|
|
30
|
+
/** The sentence for a run's outcome, or undefined when it has none (running, or succeeded). */
|
|
31
|
+
function assistantRunOutcomeText(run, t) {
|
|
32
|
+
const code = run?.outcomeCode;
|
|
33
|
+
if (!code)
|
|
34
|
+
return undefined;
|
|
35
|
+
let params = {};
|
|
36
|
+
try {
|
|
37
|
+
params = run.outcomeParams ? JSON.parse(run.outcomeParams) : {};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
params = {};
|
|
41
|
+
}
|
|
42
|
+
const english = exports.ASSISTANT_RUN_OUTCOME_ENGLISH[code] ?? code;
|
|
43
|
+
const fill = (text) => text.replace(/\{(\w+)\}/g, (_, name) => (params[name] === undefined ? `{${name}}` : String(params[name])));
|
|
44
|
+
return t ? t((0, exports.outcomeKey)(code), { ...params, defaultValue: english }) : fill(english);
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=run-outcome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-outcome.js","sourceRoot":"","sources":["../../../server/service/unattended-run/run-outcome.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAwBH,0DAYC;AAlCM,MAAM,UAAU,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,oBAAoB,IAAI,EAAE,CAAA;AAAzD,QAAA,UAAU,cAA+C;AAEzD,QAAA,6BAA6B,GAA2B;IACnE,0BAA0B,EAAE,mDAAmD;IAC/E,2BAA2B,EAAE,8DAA8D;IAC3F,6BAA6B,EAAE,sEAAsE;IACrG,gCAAgC,EAAE,yEAAyE;IAC3G,+BAA+B,EAAE,0DAA0D;IAC3F,wBAAwB,EAAE,2CAA2C;IACrE,+BAA+B,EAAE,kEAAkE;IACnG,0BAA0B,EAAE,8CAA8C;IAC1E,uBAAuB,EAAE,+CAA+C;IACxE,2BAA2B,EAAE,wCAAwC;IACrE,2BAA2B,EAAE,qCAAqC;IAClE,yBAAyB,EAAE,gDAAgD;IAC3E,mBAAmB,EAAE,mCAAmC;IACxD,wBAAwB,EAAE,yDAAyD;CACpF,CAAA;AAID,+FAA+F;AAC/F,SAAgB,uBAAuB,CAAC,GAAmE,EAAE,CAAa;IACxH,MAAM,IAAI,GAAG,GAAG,EAAE,WAAW,CAAA;IAC7B,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAA;IAC3B,IAAI,MAAM,GAA4B,EAAE,CAAA;IACxC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IACjE,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,GAAG,EAAE,CAAA;IACb,CAAC;IACD,MAAM,OAAO,GAAG,qCAA6B,CAAC,IAAI,CAAC,IAAI,IAAI,CAAA;IAC3D,MAAM,IAAI,GAAG,CAAC,IAAY,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAA;IACzI,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAA,kBAAU,EAAC,IAAI,CAAC,EAAE,EAAE,GAAG,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;AACtF,CAAC","sourcesContent":["/*\n * Why a run failed or was refused, as a person reads it (design/ai/unattended-run.md §7).\n *\n * The run keeps a code and its values (ADR-0054); the sentence is made where it is shown, under this package's key\n * `ai-assistant.run.<code>` - an outcome is a stored value, not a thrown refusal. The English below is the sentence\n * when no translation has the key; the application that shows a run does not write its own.\n */\n\nexport const outcomeKey = (code: string) => `ai-assistant.run.${code}`\n\nexport const ASSISTANT_RUN_OUTCOME_ENGLISH: Record<string, string> = {\n 'assistant-run-as-unknown': 'the account the run was to act as does not exist.',\n 'assistant-run-as-inactive': \"the owner's account is not active, so the run did not start.\",\n 'assistant-run-as-not-member': 'the owner can no longer enter this domain, so the run did not start.',\n 'assistant-run-as-not-permitted': 'the owner may no longer ask the AI assistant, so the run did not start.',\n 'assistant-mode-not-unattended': 'the assistant mode {kind} does not run without a person.',\n 'unknown-assistant-mode': 'this server has no assistant mode {kind}.',\n 'assistant-run-budget-exceeded': 'the run reached its {limit} limit ({used} of {max}) and stopped.',\n 'assistant-run-step-limit': 'the run did not finish within {steps} steps.',\n 'assistant-run-timeout': 'the run did not finish within {timeoutMs} ms.',\n 'assistant-run-empty-draft': 'the run ended without writing a draft.',\n 'assistant-run-model-error': 'the AI model failed during the run.',\n 'assistant-run-abandoned': 'the run stopped partway and was started again.',\n 'ai-not-configured': 'AI is not set up for this domain.',\n 'ai-agent-not-available': 'the AI agent picked for this run is not available here.'\n}\n\ntype Translate = (key: string, options?: Record<string, unknown>) => string\n\n/** The sentence for a run's outcome, or undefined when it has none (running, or succeeded). */\nexport function assistantRunOutcomeText(run: { outcomeCode?: string | null; outcomeParams?: string | null }, t?: Translate): string | undefined {\n const code = run?.outcomeCode\n if (!code) return undefined\n let params: Record<string, unknown> = {}\n try {\n params = run.outcomeParams ? JSON.parse(run.outcomeParams) : {}\n } catch {\n params = {}\n }\n const english = ASSISTANT_RUN_OUTCOME_ENGLISH[code] ?? code\n const fill = (text: string) => text.replace(/\\{(\\w+)\\}/g, (_, name) => (params[name] === undefined ? `{${name}}` : String(params[name])))\n return t ? t(outcomeKey(code), { ...params, defaultValue: english }) : fill(english)\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Repository } from 'typeorm';
|
|
2
|
+
import { AssistantRun } from './assistant-run.js';
|
|
3
|
+
export interface RunIdentity {
|
|
4
|
+
domainId: string;
|
|
5
|
+
source: string;
|
|
6
|
+
subject: string;
|
|
7
|
+
scheduledAt: Date;
|
|
8
|
+
}
|
|
9
|
+
export interface RunStart {
|
|
10
|
+
name: string;
|
|
11
|
+
modeKind: string;
|
|
12
|
+
modeTarget?: string;
|
|
13
|
+
/** Absent when the account named does not exist - the row still says the run was refused. */
|
|
14
|
+
runAsId?: string;
|
|
15
|
+
startedAt: Date;
|
|
16
|
+
deadlineAt: Date;
|
|
17
|
+
}
|
|
18
|
+
export type RunEnding = Partial<Pick<AssistantRun, 'status' | 'finishedAt' | 'draft' | 'reads' | 'doors' | 'outcomeCode' | 'outcomeParams' | 'groundingWarnings' | 'agentId' | 'clientId' | 'modelCalls' | 'inputTokens' | 'outputTokens' | 'cardsDropped'>>;
|
|
19
|
+
export interface RunStore {
|
|
20
|
+
/** The row of this occurrence, and whether this call holds its attempt now. */
|
|
21
|
+
claim(identity: RunIdentity, start: RunStart, now: Date): Promise<{
|
|
22
|
+
claimed: boolean;
|
|
23
|
+
row: AssistantRun;
|
|
24
|
+
}>;
|
|
25
|
+
/** Ends the attempt the caller holds. A later attempt's row is returned untouched. */
|
|
26
|
+
finish(held: {
|
|
27
|
+
id: string;
|
|
28
|
+
attempt: number;
|
|
29
|
+
}, ending: RunEnding): Promise<AssistantRun>;
|
|
30
|
+
}
|
|
31
|
+
export declare function typeormRunStore(repository: () => Repository<AssistantRun>): RunStore;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.typeormRunStore = typeormRunStore;
|
|
4
|
+
const assistant_run_js_1 = require("./assistant-run.js");
|
|
5
|
+
const run_ledger_js_1 = require("./run-ledger.js");
|
|
6
|
+
/** What a fresh attempt starts with - nothing of the attempt before it. */
|
|
7
|
+
const CLEARED = {
|
|
8
|
+
finishedAt: null,
|
|
9
|
+
draft: null,
|
|
10
|
+
reads: null,
|
|
11
|
+
doors: null,
|
|
12
|
+
outcomeCode: null,
|
|
13
|
+
outcomeParams: null,
|
|
14
|
+
groundingWarnings: null,
|
|
15
|
+
agentId: null,
|
|
16
|
+
clientId: null,
|
|
17
|
+
modelCalls: 0,
|
|
18
|
+
inputTokens: 0,
|
|
19
|
+
outputTokens: 0,
|
|
20
|
+
cardsDropped: 0
|
|
21
|
+
};
|
|
22
|
+
function endingOf(row) {
|
|
23
|
+
/* A running attempt being taken over did not end by itself - it is closed as abandoned. */
|
|
24
|
+
const abandoned = row.status === 'running';
|
|
25
|
+
return {
|
|
26
|
+
attempt: row.attempt,
|
|
27
|
+
status: abandoned ? 'failed' : row.status,
|
|
28
|
+
outcomeCode: abandoned ? 'assistant-run-abandoned' : row.outcomeCode ?? null,
|
|
29
|
+
startedAt: row.startedAt ?? null,
|
|
30
|
+
finishedAt: abandoned ? null : row.finishedAt ?? null
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function typeormRunStore(repository) {
|
|
34
|
+
const where = (identity) => ({
|
|
35
|
+
domain: { id: identity.domainId },
|
|
36
|
+
source: identity.source,
|
|
37
|
+
subject: identity.subject,
|
|
38
|
+
scheduledAt: identity.scheduledAt
|
|
39
|
+
});
|
|
40
|
+
const read = (identity) => repository().findOne({ where: where(identity) });
|
|
41
|
+
async function retake(row, start) {
|
|
42
|
+
const history = [...(row.attemptsJson ?? []), endingOf(row)];
|
|
43
|
+
const result = await repository()
|
|
44
|
+
.createQueryBuilder()
|
|
45
|
+
.update(assistant_run_js_1.AssistantRun)
|
|
46
|
+
.set({
|
|
47
|
+
...CLEARED,
|
|
48
|
+
status: 'running',
|
|
49
|
+
attempt: row.attempt + 1,
|
|
50
|
+
attempts: JSON.stringify(history),
|
|
51
|
+
startedAt: start.startedAt,
|
|
52
|
+
deadlineAt: start.deadlineAt,
|
|
53
|
+
modeKind: start.modeKind,
|
|
54
|
+
modeTarget: start.modeTarget ?? null,
|
|
55
|
+
runAs: start.runAsId ? { id: start.runAsId } : null,
|
|
56
|
+
updater: start.runAsId ? { id: start.runAsId } : null
|
|
57
|
+
})
|
|
58
|
+
.where('id = :id AND attempt = :attempt AND status = :status', { id: row.id, attempt: row.attempt, status: row.status })
|
|
59
|
+
.execute();
|
|
60
|
+
return result.affected === 1;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
async claim(identity, start, now) {
|
|
64
|
+
let row = await read(identity);
|
|
65
|
+
if (!row) {
|
|
66
|
+
try {
|
|
67
|
+
await repository().insert({
|
|
68
|
+
...where(identity),
|
|
69
|
+
name: start.name,
|
|
70
|
+
modeKind: start.modeKind,
|
|
71
|
+
modeTarget: start.modeTarget,
|
|
72
|
+
...(start.runAsId ? { runAs: { id: start.runAsId }, creator: { id: start.runAsId }, updater: { id: start.runAsId } } : {}),
|
|
73
|
+
status: 'running',
|
|
74
|
+
attempt: 1,
|
|
75
|
+
startedAt: start.startedAt,
|
|
76
|
+
deadlineAt: start.deadlineAt,
|
|
77
|
+
modelCalls: 0,
|
|
78
|
+
inputTokens: 0,
|
|
79
|
+
outputTokens: 0,
|
|
80
|
+
cardsDropped: 0
|
|
81
|
+
});
|
|
82
|
+
return { claimed: true, row: (await read(identity)) };
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
/* Another call made the row first - the unique index said so. Anything else is not that and is thrown. */
|
|
86
|
+
row = await read(identity);
|
|
87
|
+
if (!row)
|
|
88
|
+
throw error;
|
|
89
|
+
return { claimed: false, row };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if ((0, run_ledger_js_1.nextStep)(row, now) === 'return')
|
|
93
|
+
return { claimed: false, row };
|
|
94
|
+
const took = await retake(row, start);
|
|
95
|
+
return { claimed: took, row: (await read(identity)) };
|
|
96
|
+
},
|
|
97
|
+
async finish(held, ending) {
|
|
98
|
+
await repository()
|
|
99
|
+
.createQueryBuilder()
|
|
100
|
+
.update(assistant_run_js_1.AssistantRun)
|
|
101
|
+
.set(ending)
|
|
102
|
+
.where('id = :id AND attempt = :attempt AND status = :status', { id: held.id, attempt: held.attempt, status: 'running' })
|
|
103
|
+
.execute();
|
|
104
|
+
return (await repository().findOne({ where: { id: held.id } }));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=run-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-store.js","sourceRoot":"","sources":["../../../server/service/unattended-run/run-store.ts"],"names":[],"mappings":";;AAqFA,0CA4EC;AAxJD,yDAAiD;AACjD,mDAA0C;AA8C1C,2EAA2E;AAC3E,MAAM,OAAO,GAAG;IACd,UAAU,EAAE,IAAI;IAChB,KAAK,EAAE,IAAI;IACX,KAAK,EAAE,IAAI;IACX,KAAK,EAAE,IAAI;IACX,WAAW,EAAE,IAAI;IACjB,aAAa,EAAE,IAAI;IACnB,iBAAiB,EAAE,IAAI;IACvB,OAAO,EAAE,IAAI;IACb,QAAQ,EAAE,IAAI;IACd,UAAU,EAAE,CAAC;IACb,WAAW,EAAE,CAAC;IACd,YAAY,EAAE,CAAC;IACf,YAAY,EAAE,CAAC;CAChB,CAAA;AAED,SAAS,QAAQ,CAAC,GAAiB;IACjC,2FAA2F;IAC3F,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,KAAK,SAAS,CAAA;IAC1C,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM;QACzC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC,yBAAyB,CAAC,CAAC,CAAC,GAAG,CAAC,WAAW,IAAI,IAAI;QAC5E,SAAS,EAAE,GAAG,CAAC,SAAS,IAAI,IAAI;QAChC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI;KACtD,CAAA;AACH,CAAC;AAED,SAAgB,eAAe,CAAC,UAA0C;IACxE,MAAM,KAAK,GAAG,CAAC,QAAqB,EAAE,EAAE,CAAC,CAAC;QACxC,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE;QACjC,MAAM,EAAE,QAAQ,CAAC,MAAM;QACvB,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,WAAW,EAAE,QAAQ,CAAC,WAAW;KAClC,CAAC,CAAA;IAEF,MAAM,IAAI,GAAG,CAAC,QAAqB,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,QAAQ,CAAQ,EAAE,CAAC,CAAA;IAE/F,KAAK,UAAU,MAAM,CAAC,GAAiB,EAAE,KAAe;QACtD,MAAM,OAAO,GAAG,CAAC,GAAG,CAAE,GAAW,CAAC,YAAY,IAAI,EAAE,CAAC,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;QACrE,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE;aAC9B,kBAAkB,EAAE;aACpB,MAAM,CAAC,+BAAY,CAAC;aACpB,GAAG,CAAC;YACH,GAAG,OAAO;YACV,MAAM,EAAE,SAAS;YACjB,OAAO,EAAE,GAAG,CAAC,OAAO,GAAG,CAAC;YACxB,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;YACjC,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;YAC5B,QAAQ,EAAE,KAAK,CAAC,QAAQ;YACxB,UAAU,EAAE,KAAK,CAAC,UAAU,IAAI,IAAI;YACpC,KAAK,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI;YACnD,OAAO,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,IAAI;SAC/C,CAAC;aACR,KAAK,CAAC,sDAAsD,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC;aACvH,OAAO,EAAE,CAAA;QACZ,OAAO,MAAM,CAAC,QAAQ,KAAK,CAAC,CAAA;IAC9B,CAAC;IAED,OAAO;QACL,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG;YAC9B,IAAI,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAA;YAC9B,IAAI,CAAC,GAAG,EAAE,CAAC;gBACT,IAAI,CAAC;oBACH,MAAM,UAAU,EAAE,CAAC,MAAM,CAAC;wBACxB,GAAG,KAAK,CAAC,QAAQ,CAAC;wBAClB,IAAI,EAAE,KAAK,CAAC,IAAI;wBAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ;wBACxB,UAAU,EAAE,KAAK,CAAC,UAAU;wBAC5B,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;wBAC1H,MAAM,EAAE,SAAS;wBACjB,OAAO,EAAE,CAAC;wBACV,SAAS,EAAE,KAAK,CAAC,SAAS;wBAC1B,UAAU,EAAE,KAAK,CAAC,UAAU;wBAC5B,UAAU,EAAE,CAAC;wBACb,WAAW,EAAE,CAAC;wBACd,YAAY,EAAE,CAAC;wBACf,YAAY,EAAE,CAAC;qBACT,CAAC,CAAA;oBACT,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAE,EAAE,CAAA;gBACxD,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,0GAA0G;oBAC1G,GAAG,GAAG,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAA;oBAC1B,IAAI,CAAC,GAAG;wBAAE,MAAM,KAAK,CAAA;oBACrB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAA;gBAChC,CAAC;YACH,CAAC;YAED,IAAI,IAAA,wBAAQ,EAAC,GAAG,EAAE,GAAG,CAAC,KAAK,QAAQ;gBAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,CAAA;YACnE,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,CAAA;YACrC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,CAAE,EAAE,CAAA;QACxD,CAAC;QAED,KAAK,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM;YACvB,MAAM,UAAU,EAAE;iBACf,kBAAkB,EAAE;iBACpB,MAAM,CAAC,+BAAY,CAAC;iBACpB,GAAG,CAAC,MAAa,CAAC;iBAClB,KAAK,CAAC,sDAAsD,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;iBACxH,OAAO,EAAE,CAAA;YACZ,OAAO,CAAC,MAAM,UAAU,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAS,EAAE,CAAC,CAAE,CAAA;QACzE,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["/*\n * Where runs are kept - one row per occurrence, attempts on the same row (design/ai/unattended-run.md §5).\n *\n * Two calls for one occurrence at once are parted by the unique index: one insert wins, the other reads the row it\n * made. Taking an occurrence over is one update guarded by the attempt it saw, so two servers cannot both take it.\n * Finishing is guarded the same way: an attempt that was taken over and ends late does not write over the next one.\n */\nimport type { Repository } from 'typeorm'\n\nimport { AssistantRun } from './assistant-run.js'\nimport { nextStep } from './run-ledger.js'\n\nexport interface RunIdentity {\n domainId: string\n source: string\n subject: string\n scheduledAt: Date\n}\n\nexport interface RunStart {\n name: string\n modeKind: string\n modeTarget?: string\n /** Absent when the account named does not exist - the row still says the run was refused. */\n runAsId?: string\n startedAt: Date\n deadlineAt: Date\n}\n\nexport type RunEnding = Partial<\n Pick<\n AssistantRun,\n | 'status'\n | 'finishedAt'\n | 'draft'\n | 'reads'\n | 'doors'\n | 'outcomeCode'\n | 'outcomeParams'\n | 'groundingWarnings'\n | 'agentId'\n | 'clientId'\n | 'modelCalls'\n | 'inputTokens'\n | 'outputTokens'\n | 'cardsDropped'\n >\n>\n\nexport interface RunStore {\n /** The row of this occurrence, and whether this call holds its attempt now. */\n claim(identity: RunIdentity, start: RunStart, now: Date): Promise<{ claimed: boolean; row: AssistantRun }>\n /** Ends the attempt the caller holds. A later attempt's row is returned untouched. */\n finish(held: { id: string; attempt: number }, ending: RunEnding): Promise<AssistantRun>\n}\n\n/** What a fresh attempt starts with - nothing of the attempt before it. */\nconst CLEARED = {\n finishedAt: null,\n draft: null,\n reads: null,\n doors: null,\n outcomeCode: null,\n outcomeParams: null,\n groundingWarnings: null,\n agentId: null,\n clientId: null,\n modelCalls: 0,\n inputTokens: 0,\n outputTokens: 0,\n cardsDropped: 0\n}\n\nfunction endingOf(row: AssistantRun) {\n /* A running attempt being taken over did not end by itself - it is closed as abandoned. */\n const abandoned = row.status === 'running'\n return {\n attempt: row.attempt,\n status: abandoned ? 'failed' : row.status,\n outcomeCode: abandoned ? 'assistant-run-abandoned' : row.outcomeCode ?? null,\n startedAt: row.startedAt ?? null,\n finishedAt: abandoned ? null : row.finishedAt ?? null\n }\n}\n\nexport function typeormRunStore(repository: () => Repository<AssistantRun>): RunStore {\n const where = (identity: RunIdentity) => ({\n domain: { id: identity.domainId },\n source: identity.source,\n subject: identity.subject,\n scheduledAt: identity.scheduledAt\n })\n\n const read = (identity: RunIdentity) => repository().findOne({ where: where(identity) as any })\n\n async function retake(row: AssistantRun, start: RunStart): Promise<boolean> {\n const history = [...((row as any).attemptsJson ?? []), endingOf(row)]\n const result = await repository()\n .createQueryBuilder()\n .update(AssistantRun)\n .set({\n ...CLEARED,\n status: 'running',\n attempt: row.attempt + 1,\n attempts: JSON.stringify(history),\n startedAt: start.startedAt,\n deadlineAt: start.deadlineAt,\n modeKind: start.modeKind,\n modeTarget: start.modeTarget ?? null,\n runAs: start.runAsId ? { id: start.runAsId } : null,\n updater: start.runAsId ? { id: start.runAsId } : null\n } as any)\n .where('id = :id AND attempt = :attempt AND status = :status', { id: row.id, attempt: row.attempt, status: row.status })\n .execute()\n return result.affected === 1\n }\n\n return {\n async claim(identity, start, now) {\n let row = await read(identity)\n if (!row) {\n try {\n await repository().insert({\n ...where(identity),\n name: start.name,\n modeKind: start.modeKind,\n modeTarget: start.modeTarget,\n ...(start.runAsId ? { runAs: { id: start.runAsId }, creator: { id: start.runAsId }, updater: { id: start.runAsId } } : {}),\n status: 'running',\n attempt: 1,\n startedAt: start.startedAt,\n deadlineAt: start.deadlineAt,\n modelCalls: 0,\n inputTokens: 0,\n outputTokens: 0,\n cardsDropped: 0\n } as any)\n return { claimed: true, row: (await read(identity))! }\n } catch (error) {\n /* Another call made the row first - the unique index said so. Anything else is not that and is thrown. */\n row = await read(identity)\n if (!row) throw error\n return { claimed: false, row }\n }\n }\n\n if (nextStep(row, now) === 'return') return { claimed: false, row }\n const took = await retake(row, start)\n return { claimed: took, row: (await read(identity))! }\n },\n\n async finish(held, ending) {\n await repository()\n .createQueryBuilder()\n .update(AssistantRun)\n .set(ending as any)\n .where('id = :id AND attempt = :attempt AND status = :status', { id: held.id, attempt: held.attempt, status: 'running' })\n .execute()\n return (await repository().findOne({ where: { id: held.id } as any }))!\n }\n }\n}\n"]}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type AgentClient, type AiClientRequest, type ToolAccessCheck } from '@things-factory/ai-client-base';
|
|
2
|
+
import { AssistantRun } from './assistant-run.js';
|
|
3
|
+
import { type RunAsChecks } from './run-as.js';
|
|
4
|
+
import { type RunStore } from './run-store.js';
|
|
5
|
+
export interface UnattendedRunInput {
|
|
6
|
+
/** The domain the run is in, as loaded - its parent and owner decide who can enter it. */
|
|
7
|
+
domain: {
|
|
8
|
+
id: string;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
};
|
|
11
|
+
/** The user the run acts as - the policy owner. Their privileges and doors only. */
|
|
12
|
+
runAs: string;
|
|
13
|
+
/** Which occurrence this is. The same three again is the same run. */
|
|
14
|
+
run: {
|
|
15
|
+
/** Who scheduled it - `<app>.<work>`, e.g. `office.strategy-review`. */
|
|
16
|
+
source: string;
|
|
17
|
+
/** What it is about in the source's terms - a policy id. */
|
|
18
|
+
subject: string;
|
|
19
|
+
scheduledAt: Date;
|
|
20
|
+
};
|
|
21
|
+
/** A registered mode that declares `unattended`. */
|
|
22
|
+
mode: {
|
|
23
|
+
kind: string;
|
|
24
|
+
target?: string;
|
|
25
|
+
};
|
|
26
|
+
/** What this occurrence is asked to do - the caller server's text, read by the model (English). */
|
|
27
|
+
instruction: string;
|
|
28
|
+
/** What the run is about, for the tools. Filled by the caller's server; still never used for authorization. */
|
|
29
|
+
hostContext?: Record<string, any>;
|
|
30
|
+
/** The language of the draft - the owner's. Absent: their account's locale. */
|
|
31
|
+
lng?: string;
|
|
32
|
+
}
|
|
33
|
+
/** What the run reaches outside itself. The defaults are the server's; the tests hand their own. */
|
|
34
|
+
export interface UnattendedRunDeps {
|
|
35
|
+
store: RunStore;
|
|
36
|
+
runAs: RunAsChecks<any, any>;
|
|
37
|
+
clientFor: (request: AiClientRequest) => Promise<AgentClient>;
|
|
38
|
+
toolAccess: (state: any) => ToolAccessCheck;
|
|
39
|
+
now: () => Date;
|
|
40
|
+
}
|
|
41
|
+
export declare function runAssistantUnattended(input: UnattendedRunInput, deps?: UnattendedRunDeps): Promise<AssistantRun>;
|
|
42
|
+
/** The stored run of an occurrence, for the application that scheduled it - read behind that application's own door. */
|
|
43
|
+
export declare function findAssistantRun(identity: {
|
|
44
|
+
domainId: string;
|
|
45
|
+
source: string;
|
|
46
|
+
subject: string;
|
|
47
|
+
scheduledAt: Date;
|
|
48
|
+
} | {
|
|
49
|
+
id: string;
|
|
50
|
+
}): Promise<AssistantRun | null>;
|