@stratusagent/gateway 0.14.2 → 0.14.4
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/LICENSE +202 -21
- package/NOTICE +5 -0
- package/README.md +5 -1
- package/dist/index.d.ts +39 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +189 -22
- package/dist/index.js.map +1 -1
- package/dist/memory-passes.d.ts +137 -0
- package/dist/memory-passes.d.ts.map +1 -0
- package/dist/memory-passes.js +675 -0
- package/dist/memory-passes.js.map +1 -0
- package/launchd/com.stratuslabs.stratusd.plist +1 -1
- package/package.json +11 -10
package/dist/index.js
CHANGED
|
@@ -9,9 +9,10 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
9
9
|
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
|
10
10
|
import { mkdir, readFile, rename, rm } from 'node:fs/promises';
|
|
11
11
|
import path from 'node:path';
|
|
12
|
-
import { filePathsOf, isUnaddressedTurn, carriesKey, leastTrusted, senderTrustOf, SENDER_TRUST_METADATA_KEY, workItemState, turnMessageOf, turnKeyOf, leftForNextStart, lastResponseIsAnswer, MAX_IDEMPOTENCY_KEY_LENGTH, recordTurnFailure, completeAnsweredTurn, abortErrorFor, sessionTrustOf, sessionWriteTrust, AgentRegistry, AgentRunner, ChannelRegistry, ContributionRegistry, createRoutedMemoryStore, EventBus, RunAbortedError, SkillRegistry, ToolRegistry, createSkillReadTool, missingSkillRequirements, describeToolAllowlistFinding, unmatchedToolAllowlist, CONTEXT_FLOOR_METADATA_KEY, CONTEXT_SUMMARY_METADATA_KEY, PENDING_APPROVAL_METADATA_KEY, SESSION_TAINTED_BY_METADATA_KEY, SESSION_TRUST_METADATA_KEY, latestTurnReply, readPendingApproval, conversationContextFrom, CONVERSATION_METADATA_KEY, STRATUS_SKILL_ID, imagesWithinReplayBudget, matchesToolAllowlist, } from '@stratusagent/core';
|
|
13
|
-
import { CREDENTIAL_REQUEST_TOOL_NAME, createCredentialRequestTool, createDelegateTool, DELEGATE_TOOL_NAME, createForgetTool, createPinTool, createMessageSendTool, createMessageReadTool, createChannelArchiveTool, createChannelCreateTool, createChannelInviteTool, createChannelJoinTool, directMessageReadRefusal, createRecallTool, createRememberTool, createScheduleTools, DELEGATED_BY_METADATA_KEY, DELEGATED_SESSION_ID_MARKER, DEFAULT_MAX_DELEGATION_DEPTH, DELEGATION_DEPTH_METADATA_KEY, delegatingSessionIdOf, isDelegatedSession, outstandingDelegationFor, ROOT_SESSION_ID_METADATA_KEY, SCHEDULE_ID_METADATA_KEY, SCHEDULED_TURN_METADATA_KEY, withoutDelegation, loadStratusSkill, STRATUS_SKILL_PATH, credentialVariableProblem, } from '@stratusagent/agents';
|
|
12
|
+
import { filePathsOf, isUnaddressedTurn, carriesKey, leastTrusted, senderTrustOf, SENDER_TRUST_METADATA_KEY, workItemState, turnMessageOf, turnKeyOf, leftForNextStart, lastResponseIsAnswer, MAX_IDEMPOTENCY_KEY_LENGTH, recordTurnFailure, completeAnsweredTurn, abortErrorFor, sessionTrustOf, sessionWriteTrust, AgentRegistry, AgentRunner, InMemorySessionStore, MEMORY_PASS_MAX_MODEL_CALLS, MEMORY_PASS_RULES, ChannelRegistry, ContributionRegistry, createRoutedMemoryStore, EventBus, RunAbortedError, SkillRegistry, ToolRegistry, createSkillReadTool, missingSkillRequirements, describeToolAllowlistFinding, unmatchedToolAllowlist, CONTEXT_FLOOR_METADATA_KEY, CONTEXT_SUMMARY_METADATA_KEY, PENDING_APPROVAL_METADATA_KEY, SESSION_TAINTED_BY_METADATA_KEY, SESSION_TRUST_METADATA_KEY, latestTurnReply, readPendingApproval, conversationContextFrom, CONVERSATION_METADATA_KEY, STRATUS_SKILL_ID, imagesWithinReplayBudget, matchesToolAllowlist, resolveToolRisk, } from '@stratusagent/core';
|
|
13
|
+
import { CREDENTIAL_REQUEST_TOOL_NAME, createCredentialRequestTool, createDelegateTool, DELEGATE_TOOL_NAME, createForgetTool, createPinTool, createMessageSendTool, createMessageSayTool, createMessageReadTool, createChannelArchiveTool, createChannelCreateTool, createChannelInviteTool, createChannelJoinTool, directMessageReadRefusal, createRecallTool, createRememberTool, createScheduleTools, DELEGATED_BY_METADATA_KEY, DELEGATED_SESSION_ID_MARKER, DEFAULT_MAX_DELEGATION_DEPTH, DELEGATION_DEPTH_METADATA_KEY, delegatingSessionIdOf, isDelegatedSession, outstandingDelegationFor, ROOT_SESSION_ID_METADATA_KEY, SCHEDULE_ID_METADATA_KEY, SCHEDULED_TURN_METADATA_KEY, withoutDelegation, loadStratusSkill, STRATUS_SKILL_PATH, credentialVariableProblem, } from '@stratusagent/agents';
|
|
14
14
|
import { createFleetReader } from "./fleet.js";
|
|
15
|
+
import { createMemoryPassHost } from "./memory-passes.js";
|
|
15
16
|
import { createEventIntake, summarizeEvents, } from "./event-intake.js";
|
|
16
17
|
import { SqliteEventStore } from "./events.js";
|
|
17
18
|
import { createSchedulerRuntime, isScheduleSessionId, SqliteScheduleStore, SCHEDULE_SESSION_ID_PREFIX, } from "./schedules.js";
|
|
@@ -23,7 +24,7 @@ export { SqliteEventStore, EVENT_BLOCK_MAX_BYTES, EVENT_DEDUP_WINDOW_MS, EVENT_D
|
|
|
23
24
|
export { createEventIntake, mapGithubDelivery, parseStandardWebhookEvent, verifyGithubSignature, verifyStandardWebhookSignature, DEFAULT_EVENT_MAX_BODY_BYTES, EVENT_SOURCE_REQUESTS_PER_MINUTE, } from "./event-intake.js";
|
|
24
25
|
import { createLocalCommandExecutor } from '@stratusagent/executor-local';
|
|
25
26
|
import { loadPlugins, } from '@stratusagent/plugins';
|
|
26
|
-
import { BUILTIN_EXECUTOR_NAME, BUILTIN_MEMORY_STORE_NAME, BUILTIN_PROVIDER_NAMES, createDemoTool, createFileCredentialResolver, resolveDaemonCredential, createHomeMemoryStore, drainSharedMemory, hasBracketedLegacyStateIn, createRuntimeProvider, describeServingModel, DEFAULT_STRATUS_AGENT, isRegisteredProviderName, loadChannelTransportSecrets, registeredProviderNameOf, loadOperatorSkills, loadRosterSouls, FALLBACK_ACTIVE_METADATA_KEY, loadSoulFile, grantSoulCredential, withSoulFileLock, addNamedCredential, appendCredentialAudit, replaceNamedCredential, NamedCredentialMissingError, namedCredentialSource, NamedCredentialExistsError, CREDENTIAL_NAME_PATTERN, quoteShellArg, migrateLegacyMemory, PROVIDER_STATE_METADATA_KEYS, ConfigFileError, loadConfigFile, resolveConfigLocation, resolveConfiguredSoul, resolveRuntimeConfig, runsOnHarness, applySoulPins, assertStateCompatible, stratusHomePath, intakeStatePath, fleetDbIn, createAgentWorkspaces, createPluginStateDirectories, createHostProtectedPaths, foldedAgentId, syncDirectory, writeFileSynced, } from '@stratusagent/state';
|
|
27
|
+
import { BUILTIN_EXECUTOR_NAME, BUILTIN_MEMORY_STORE_NAME, BUILTIN_PROVIDER_NAMES, createDemoTool, createFileCredentialResolver, resolveDaemonCredential, createHomeMemoryStore, drainSharedMemory, hasBracketedLegacyStateIn, createRuntimeProvider, describeServingModel, DEFAULT_STRATUS_AGENT, isRegisteredProviderName, loadChannelTransportSecrets, registeredProviderNameOf, loadOperatorSkills, loadRosterSouls, FALLBACK_ACTIVE_METADATA_KEY, loadSoulFile, grantSoulCredential, withSoulFileLock, addNamedCredential, appendCredentialAudit, replaceNamedCredential, NamedCredentialMissingError, namedCredentialSource, NamedCredentialExistsError, CREDENTIAL_NAME_PATTERN, quoteShellArg, migrateLegacyMemory, PROVIDER_STATE_METADATA_KEYS, ConfigFileError, loadConfigFile, resolveConfigLocation, resolveConfiguredSoul, resolveRuntimeConfig, runsOnHarness, applySoulPins, assertStateCompatible, unlistedToolsFor, stratusHomePath, intakeStatePath, fleetDbIn, createAgentWorkspaces, createPluginStateDirectories, createHostProtectedPaths, foldedAgentId, syncDirectory, writeFileSynced, } from '@stratusagent/state';
|
|
27
28
|
const DEFAULT_IDLE_TIMEOUT_MS = 120_000;
|
|
28
29
|
/**
|
|
29
30
|
* How long a parked call waits for a person. Long enough to survive a
|
|
@@ -419,6 +420,8 @@ export const createGateway = (options = {}) => {
|
|
|
419
420
|
// process-global.
|
|
420
421
|
const fileMemory = createHomeMemoryStore(env);
|
|
421
422
|
let selectedMemory = fileMemory;
|
|
423
|
+
// The built-in store takes a list in `supersedes`; a plugin's says so.
|
|
424
|
+
let selectedMemorySupersedesMany = true;
|
|
422
425
|
const memory = createRoutedMemoryStore(() => selectedMemory);
|
|
423
426
|
let selectedExecutor;
|
|
424
427
|
// What loaded plugins contributed besides tools and skills. The loader
|
|
@@ -985,15 +988,27 @@ export const createGateway = (options = {}) => {
|
|
|
985
988
|
// the file on disk is temporarily broken (an operator mid-edit).
|
|
986
989
|
let lastGoodConfigSnapshot;
|
|
987
990
|
/**
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
*
|
|
991
|
-
|
|
992
|
-
|
|
991
|
+
* Read from the home once, on first use, which is after the migrations
|
|
992
|
+
* `serve` runs before it starts the gateway. It changes only when 0005
|
|
993
|
+
* first records it, and that happens before a daemon of this build runs.
|
|
994
|
+
*/
|
|
995
|
+
let unlistedToolsRead;
|
|
996
|
+
const unlistedTools = () => options.unlistedTools ?? (unlistedToolsRead ??= unlistedToolsFor(env));
|
|
997
|
+
/**
|
|
998
|
+
* Said at load, because an omitted `tools` list is a quiet downgrade on a
|
|
999
|
+
* home this build set up: it is only the tools registered as safe, so an
|
|
1000
|
+
* agent whose soul never wrote the key cannot run a shell, write a file
|
|
1001
|
+
* or send a message, and nothing else says why. On a home an older build
|
|
1002
|
+
* set up it is every tool, as it always was there, and the line says so
|
|
1003
|
+
* the way it always did. The built-in agent is exempt: it has no file,
|
|
1004
|
+
* and says `tools: ["*"]` in code.
|
|
993
1005
|
*/
|
|
994
1006
|
const warnNoToolsList = (agentId, soulPath) => {
|
|
995
|
-
warn(
|
|
996
|
-
|
|
1007
|
+
warn(unlistedTools() === 'every'
|
|
1008
|
+
? `agent ${agentId} has no tools: list, so it may call every tool this daemon loads — `
|
|
1009
|
+
+ `add tools: [...] to ${soulPath} to say which`
|
|
1010
|
+
: `agent ${agentId} has no tools: list, so it gets only safe tools (reads and memory) — `
|
|
1011
|
+
+ `add tools: [...] to ${soulPath} to give it more, or tools: ["*"] for every tool`);
|
|
997
1012
|
};
|
|
998
1013
|
const defaultAgentId = async () => {
|
|
999
1014
|
// Identity only — never full runtime resolution: credential checks do
|
|
@@ -1432,6 +1447,23 @@ export const createGateway = (options = {}) => {
|
|
|
1432
1447
|
const ts = typeof posted === 'object' && posted !== null ? posted.ts : undefined;
|
|
1433
1448
|
return typeof ts === 'string' && ts.length > 0 ? { id: ts } : {};
|
|
1434
1449
|
}, { defaultThread: (session, destination) => scheduler.reportThreadFor(session, destination) }));
|
|
1450
|
+
// A mid-task message goes where the conversation is, through the channel
|
|
1451
|
+
// carrying this agent there, and nowhere else (card 1009). A session no
|
|
1452
|
+
// channel started (a terminal run, a schedule nobody watches) has nobody
|
|
1453
|
+
// to say it to before the reply.
|
|
1454
|
+
tools.register(createMessageSayTool(async ({ session, text }) => {
|
|
1455
|
+
const agentId = session.agent.id;
|
|
1456
|
+
const kind = session.metadata?.channel;
|
|
1457
|
+
const { adapter } = typeof kind === 'string'
|
|
1458
|
+
? channelCarrying(kind, agentId, (candidate) => candidate.say !== undefined)
|
|
1459
|
+
: { adapter: undefined };
|
|
1460
|
+
if (!adapter?.say) {
|
|
1461
|
+
throw new Error(typeof kind === 'string'
|
|
1462
|
+
? `This conversation's channel (${kind}) cannot take a mid-task message. Say it in your reply.`
|
|
1463
|
+
: 'This conversation is not in a channel that can take a mid-task message. Say it in your reply.');
|
|
1464
|
+
}
|
|
1465
|
+
await adapter.say({ sessionId: session.id, agentId, text });
|
|
1466
|
+
}));
|
|
1435
1467
|
// The same carrying rule again: a channel is created by the asking
|
|
1436
1468
|
// agent's own app, whose token never leaves the adapter, and so becomes
|
|
1437
1469
|
// that app's to post in and read.
|
|
@@ -1722,7 +1754,13 @@ export const createGateway = (options = {}) => {
|
|
|
1722
1754
|
// bearer credential the model is handed, and whoever it reaches can
|
|
1723
1755
|
// store a key and amend the soul. That is the operator's to allow.
|
|
1724
1756
|
const allowlist = session.agent.tools ?? registry.get(agentId)?.tools;
|
|
1725
|
-
|
|
1757
|
+
// An omitted list grants the safe tools, this one among them, but a
|
|
1758
|
+
// link is the operator's to allow by naming it, so only a list says yes.
|
|
1759
|
+
// On a home an older build set up an omitted list is every tool, and
|
|
1760
|
+
// has always allowed a link there.
|
|
1761
|
+
const linkAllowed = allowlist === undefined
|
|
1762
|
+
? unlistedTools() === 'every'
|
|
1763
|
+
: matchesToolAllowlist(CREDENTIAL_REQUEST_TOOL_NAME, allowlist);
|
|
1726
1764
|
// Why no form went up, said to the agent beside the link that replaces
|
|
1727
1765
|
// it, so it can tell the person why they are getting a link instead.
|
|
1728
1766
|
let formUnavailable;
|
|
@@ -2125,7 +2163,9 @@ export const createGateway = (options = {}) => {
|
|
|
2125
2163
|
if (!Array.isArray(requested) || requested.some((name) => typeof name !== 'string' || name.length === 0)) {
|
|
2126
2164
|
return 'A sub-agent\'s tools are a list of tool names.';
|
|
2127
2165
|
}
|
|
2128
|
-
|
|
2166
|
+
// Read the way the runner reads it for the parent's own calls: the
|
|
2167
|
+
// session's copy, else the registered agent's.
|
|
2168
|
+
const allowlist = parent.agent.tools ?? registry.get(parent.agent.id)?.tools;
|
|
2129
2169
|
for (const name of requested) {
|
|
2130
2170
|
if (/[*?]/.test(name)) {
|
|
2131
2171
|
return `A sub-agent's tools are exact names, and ${name} is a pattern.`;
|
|
@@ -2133,12 +2173,21 @@ export const createGateway = (options = {}) => {
|
|
|
2133
2173
|
if (subAgentNeverGets(name)) {
|
|
2134
2174
|
return `${name} is never given to a sub-agent, even by an agent that holds it.`;
|
|
2135
2175
|
}
|
|
2136
|
-
|
|
2176
|
+
const tool = tools.get(name);
|
|
2177
|
+
if (!tool) {
|
|
2137
2178
|
return `There is no tool named ${name}.`;
|
|
2138
2179
|
}
|
|
2139
2180
|
// The parent's allowlist as its running turn holds it: a slice can
|
|
2140
|
-
// only narrow what that turn may already do.
|
|
2141
|
-
|
|
2181
|
+
// only narrow what that turn may already do. With no list, that is
|
|
2182
|
+
// the tools registered as safe (every tool on a home an older build
|
|
2183
|
+
// set up), and the slice is held to the same rule: naming a gated
|
|
2184
|
+
// tool must not hand it to the sub-agent.
|
|
2185
|
+
if (allowlist === undefined) {
|
|
2186
|
+
if (unlistedTools() !== 'every' && resolveToolRisk(tool) !== 'safe') {
|
|
2187
|
+
return `${name} is not a safe tool, and ${parent.agent.id}'s soul has no tools: list, so it gets only safe tools and a sub-agent of it cannot have ${name}.`;
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2190
|
+
else if (!matchesToolAllowlist(name, allowlist)) {
|
|
2142
2191
|
return `${name} is not one of ${parent.agent.id}'s tools, so a sub-agent of it cannot have it.`;
|
|
2143
2192
|
}
|
|
2144
2193
|
}
|
|
@@ -2319,6 +2368,7 @@ export const createGateway = (options = {}) => {
|
|
|
2319
2368
|
// Asking a person for a key is harmless to grant: they decide
|
|
2320
2369
|
// everything after the question, so no soul has to list it.
|
|
2321
2370
|
grantedToEveryAgent: subAgent ? [] : [CREDENTIAL_REQUEST_TOOL_NAME],
|
|
2371
|
+
unlistedTools: unlistedTools(),
|
|
2322
2372
|
...(subAgent ? {} : { skills: skillCatalog, memory }),
|
|
2323
2373
|
streaming: true,
|
|
2324
2374
|
...(maxTurns !== undefined ? { maxTurns } : {}),
|
|
@@ -2861,7 +2911,13 @@ export const createGateway = (options = {}) => {
|
|
|
2861
2911
|
* soul. Only that path passes it.
|
|
2862
2912
|
*/
|
|
2863
2913
|
const dispatchInternal = async (input, absorbedKeys = [], subAgent,
|
|
2864
|
-
/**
|
|
2914
|
+
/**
|
|
2915
|
+
* See `RunInput.steer`; passed by `dispatch` for an opted-in caller.
|
|
2916
|
+
* Handed the turn's own signal — the one the runner was given, which
|
|
2917
|
+
* the watchdog, `abortTurns` and a restart's window abort as well as
|
|
2918
|
+
* the callers — so it takes nothing into a turn that is already
|
|
2919
|
+
* stopping (card 959).
|
|
2920
|
+
*/
|
|
2865
2921
|
steer) => {
|
|
2866
2922
|
// Re-read on every turn, not once at start: a newer build that stamps
|
|
2867
2923
|
// the home while this daemon is serving has formats this build does not
|
|
@@ -2982,12 +3038,13 @@ export const createGateway = (options = {}) => {
|
|
|
2982
3038
|
userMessage: input.userMessage,
|
|
2983
3039
|
...(input.images !== undefined ? { images: input.images } : {}),
|
|
2984
3040
|
...(input.addressed !== undefined ? { addressed: input.addressed } : {}),
|
|
3041
|
+
...(input.replyOptional === true ? { replyOptional: true } : {}),
|
|
2985
3042
|
...(input.idempotencyKey !== undefined ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
2986
3043
|
...(absorbedKeys.length > 0 ? { absorbedKeys: [...absorbedKeys] } : {}),
|
|
2987
3044
|
...(hostedLoop ? { hostedLoop } : {}),
|
|
2988
3045
|
...(input.metadata ? { metadata: input.metadata } : {}),
|
|
2989
3046
|
runtime: runtimeContextFor(source, config, switchedToFallback, input.metadata),
|
|
2990
|
-
...(steer !== undefined ? { steer } : {}),
|
|
3047
|
+
...(steer !== undefined ? { steer: () => steer(signal) } : {}),
|
|
2991
3048
|
signal,
|
|
2992
3049
|
});
|
|
2993
3050
|
}
|
|
@@ -2997,13 +3054,14 @@ export const createGateway = (options = {}) => {
|
|
|
2997
3054
|
userMessage: input.userMessage,
|
|
2998
3055
|
...(input.images !== undefined ? { images: input.images } : {}),
|
|
2999
3056
|
...(input.addressed !== undefined ? { addressed: input.addressed } : {}),
|
|
3057
|
+
...(input.replyOptional === true ? { replyOptional: true } : {}),
|
|
3000
3058
|
...(input.idempotencyKey !== undefined ? { idempotencyKey: input.idempotencyKey } : {}),
|
|
3001
3059
|
...(absorbedKeys.length > 0 ? { absorbedKeys: [...absorbedKeys] } : {}),
|
|
3002
3060
|
...(hostedLoop ? { hostedLoop } : {}),
|
|
3003
3061
|
...(input.earlier !== undefined && input.earlier.length > 0 ? { earlier: input.earlier } : {}),
|
|
3004
3062
|
metadata,
|
|
3005
3063
|
runtime: runtimeContextFor(source, config, switchedToFallback, input.metadata, subAgent !== undefined),
|
|
3006
|
-
...(steer !== undefined ? { steer } : {}),
|
|
3064
|
+
...(steer !== undefined ? { steer: () => steer(signal) } : {}),
|
|
3007
3065
|
signal,
|
|
3008
3066
|
});
|
|
3009
3067
|
}));
|
|
@@ -3627,6 +3685,88 @@ export const createGateway = (options = {}) => {
|
|
|
3627
3685
|
});
|
|
3628
3686
|
return turn;
|
|
3629
3687
|
};
|
|
3688
|
+
// ---- memory passes (spec 962) -------------------------------------------
|
|
3689
|
+
/**
|
|
3690
|
+
* A pass's one model turn: the agent's provider and credentials, exactly
|
|
3691
|
+
* the model asked for and never the fallback (cost stays what the
|
|
3692
|
+
* operator chose), only the pass's two tools, no persona, skills, memory
|
|
3693
|
+
* injection or delegates, and a session that is never stored. Its calls
|
|
3694
|
+
* go through the same provider-call guards as every turn, and its events
|
|
3695
|
+
* through the same bus, so a budget sees and counts them.
|
|
3696
|
+
*/
|
|
3697
|
+
const runMemoryPassModel = async (run) => {
|
|
3698
|
+
const source = await refreshAgent(run.agentId);
|
|
3699
|
+
const resolved = await runtimeForAgent(source);
|
|
3700
|
+
const { fallback: _fallback, ...primary } = resolved;
|
|
3701
|
+
const config = (run.model !== undefined ? { ...primary, model: run.model } : primary);
|
|
3702
|
+
const tools = new ToolRegistry();
|
|
3703
|
+
for (const tool of run.tools) {
|
|
3704
|
+
tools.register(tool);
|
|
3705
|
+
}
|
|
3706
|
+
// A step budget of n is n tool steps and one wrap-up, so this is the
|
|
3707
|
+
// pass's model-call cap.
|
|
3708
|
+
const maxTurns = MEMORY_PASS_MAX_MODEL_CALLS - 1;
|
|
3709
|
+
let passRunner;
|
|
3710
|
+
const provider = createRuntimeProvider(config, () => { }, async (session, call, context) => {
|
|
3711
|
+
if (!passRunner) {
|
|
3712
|
+
throw new Error('The Stratus runtime is not ready to execute tools yet.');
|
|
3713
|
+
}
|
|
3714
|
+
return passRunner.executeHostedToolCall(session, call, context);
|
|
3715
|
+
}, maxTurns, async () => { }, providerContributions);
|
|
3716
|
+
passRunner = new AgentRunner({
|
|
3717
|
+
provider,
|
|
3718
|
+
tools,
|
|
3719
|
+
store: new InMemorySessionStore(),
|
|
3720
|
+
bus,
|
|
3721
|
+
maxTurns,
|
|
3722
|
+
providerCallGuards,
|
|
3723
|
+
});
|
|
3724
|
+
const definition = {
|
|
3725
|
+
id: source.definition.id,
|
|
3726
|
+
name: source.definition.name,
|
|
3727
|
+
instructions: MEMORY_PASS_RULES,
|
|
3728
|
+
tools: run.tools.map((tool) => tool.name),
|
|
3729
|
+
skills: [],
|
|
3730
|
+
delegates: [],
|
|
3731
|
+
...(source.definition.credentials !== undefined ? { credentials: [...source.definition.credentials] } : {}),
|
|
3732
|
+
};
|
|
3733
|
+
await passRunner.run({
|
|
3734
|
+
sessionId: run.sessionId,
|
|
3735
|
+
agent: definition,
|
|
3736
|
+
userMessage: run.prompt,
|
|
3737
|
+
metadata: {
|
|
3738
|
+
provider: config.provider,
|
|
3739
|
+
...(config.provider !== 'demo' && config.model !== undefined ? { model: config.model } : {}),
|
|
3740
|
+
// The slice's label: the pass reads at the conversation's trust.
|
|
3741
|
+
[SENDER_TRUST_METADATA_KEY]: run.trust,
|
|
3742
|
+
},
|
|
3743
|
+
signal: run.signal,
|
|
3744
|
+
});
|
|
3745
|
+
};
|
|
3746
|
+
const memoryPassHost = createMemoryPassHost({
|
|
3747
|
+
// Beside each agent's memory.jsonl, under the home the memory store uses.
|
|
3748
|
+
stateDir: stratusHomePath(env),
|
|
3749
|
+
stateHome: stratusHomePath(env),
|
|
3750
|
+
run: randomUUID(),
|
|
3751
|
+
memory,
|
|
3752
|
+
supersedesMany: () => selectedMemorySupersedesMany,
|
|
3753
|
+
sessions: store,
|
|
3754
|
+
chain: {
|
|
3755
|
+
idle: (sessionId) => !sessionChains.has(sessionId),
|
|
3756
|
+
queue: (sessionId, work) => onSessionChain(sessionId, work),
|
|
3757
|
+
},
|
|
3758
|
+
hasAgent: (agentId) => sources.has(agentId),
|
|
3759
|
+
isSubAgentSession: (session) => isSubAgentSession(session),
|
|
3760
|
+
defaultModel: async (agentId) => {
|
|
3761
|
+
const config = await runtimeForAgent(await refreshAgent(agentId));
|
|
3762
|
+
return config.provider !== 'demo' ? config.model : undefined;
|
|
3763
|
+
},
|
|
3764
|
+
runModel: runMemoryPassModel,
|
|
3765
|
+
emit: (event) => {
|
|
3766
|
+
void bus.emit(event);
|
|
3767
|
+
},
|
|
3768
|
+
log,
|
|
3769
|
+
});
|
|
3630
3770
|
/**
|
|
3631
3771
|
* A scheduled firing's own dispatch — single-flight and stopping-aware
|
|
3632
3772
|
* like the public one, but permitted into the reserved `schedule:`
|
|
@@ -3923,6 +4063,7 @@ export const createGateway = (options = {}) => {
|
|
|
3923
4063
|
...(head.input.agentId !== undefined ? { agentId: head.input.agentId } : {}),
|
|
3924
4064
|
userMessage: runnable.map((part) => part.input.userMessage).join(MERGED_SEPARATOR),
|
|
3925
4065
|
...(images.length > 0 ? { images } : {}),
|
|
4066
|
+
...(runnable.every((part) => part.input.replyOptional === true) ? { replyOptional: true } : {}),
|
|
3926
4067
|
...(head.input.earlier !== undefined ? { earlier: head.input.earlier } : {}),
|
|
3927
4068
|
...(metadata !== undefined ? { metadata } : {}),
|
|
3928
4069
|
...(signal !== undefined ? { signal } : {}),
|
|
@@ -3983,9 +4124,15 @@ export const createGateway = (options = {}) => {
|
|
|
3983
4124
|
// the others' answer with it. A turn with a caller who cannot cancel
|
|
3984
4125
|
// has no signal to join.
|
|
3985
4126
|
const joint = turnInput.signal !== undefined ? joinableSignal([turnInput.signal]) : undefined;
|
|
4127
|
+
// Admitted on the turn's own signal, not only the callers' joint one:
|
|
4128
|
+
// the watchdog, `abortTurns` and a restart's window stop the turn
|
|
4129
|
+
// without touching its callers, and a batch taken into a turn stopped
|
|
4130
|
+
// while the store was read would share its failure instead of running
|
|
4131
|
+
// next (card 959). The turn's signal follows the joint one, so this
|
|
4132
|
+
// covers every caller giving up too.
|
|
3986
4133
|
const steer = mergeable
|
|
3987
|
-
? () => takeSteered(input.sessionId, input.agentId, steered, (parts) => {
|
|
3988
|
-
if (
|
|
4134
|
+
? (turnSignal) => takeSteered(input.sessionId, input.agentId, steered, (parts) => {
|
|
4135
|
+
if (turnSignal.aborted) {
|
|
3989
4136
|
return false;
|
|
3990
4137
|
}
|
|
3991
4138
|
for (const part of parts) {
|
|
@@ -4191,6 +4338,7 @@ export const createGateway = (options = {}) => {
|
|
|
4191
4338
|
log,
|
|
4192
4339
|
warn,
|
|
4193
4340
|
subAgents,
|
|
4341
|
+
memoryPasses: memoryPassHost.handle,
|
|
4194
4342
|
// Metadata only, from the reads the control API makes, with a log
|
|
4195
4343
|
// line per read naming the plugin. See `createFleetReader`.
|
|
4196
4344
|
fleet: (packageName) => createFleetReader({
|
|
@@ -4290,6 +4438,7 @@ export const createGateway = (options = {}) => {
|
|
|
4290
4438
|
+ `. Enable the plugin that contributes it, or set memoryStore to ${BUILTIN_MEMORY_STORE_NAME}.`);
|
|
4291
4439
|
}
|
|
4292
4440
|
selectedMemory = contribution.store;
|
|
4441
|
+
selectedMemorySupersedesMany = contribution.supersedesMany === true;
|
|
4293
4442
|
log(`memory store ${memoryName} selected, from ${contributorOf('memory', memoryName) ?? 'a plugin'}`);
|
|
4294
4443
|
}
|
|
4295
4444
|
};
|
|
@@ -4361,6 +4510,10 @@ export const createGateway = (options = {}) => {
|
|
|
4361
4510
|
+ `${reconciled.released.length} claim(s) released`);
|
|
4362
4511
|
}
|
|
4363
4512
|
await loadRoster();
|
|
4513
|
+
// Agents loaded and the memory store chosen: a plugin that turned memory
|
|
4514
|
+
// passes on in its setup had neither, so the claims a crash left open
|
|
4515
|
+
// are settled now, before any turn can end and ask for a pass.
|
|
4516
|
+
await memoryPassHost.ready();
|
|
4364
4517
|
intakeState = await readIntakeState(env, warn);
|
|
4365
4518
|
if (intakeState.paused) {
|
|
4366
4519
|
log(`intake is paused (since ${intakeState.since ?? 'unknown'}); new work will be refused until it resumes`);
|
|
@@ -4604,6 +4757,12 @@ export const createGateway = (options = {}) => {
|
|
|
4604
4757
|
// already past its `stopping` check loses to `dispatch`'s own
|
|
4605
4758
|
// refusal — the second gate is why this needs no handshake.
|
|
4606
4759
|
scheduler.stop();
|
|
4760
|
+
// Memory passes are low priority: aborted at once rather than drained,
|
|
4761
|
+
// settled from what they wrote, then the stop records. Awaited before
|
|
4762
|
+
// the stores close, since a settle reads the memory store.
|
|
4763
|
+
const passesClosed = memoryPassHost.close().catch((error) => {
|
|
4764
|
+
warn(`memory passes did not close cleanly: ${error instanceof Error ? error.message : String(error)}`);
|
|
4765
|
+
});
|
|
4607
4766
|
// Deny what is parked before draining, or the drain waits out every
|
|
4608
4767
|
// outstanding approval timeout — a shutdown would hang for as long as
|
|
4609
4768
|
// the longest request had left. Each denial is a real decision the
|
|
@@ -4642,6 +4801,7 @@ export const createGateway = (options = {}) => {
|
|
|
4642
4801
|
return channelsStopping;
|
|
4643
4802
|
};
|
|
4644
4803
|
const drain = (async () => {
|
|
4804
|
+
await passesClosed;
|
|
4645
4805
|
// A promotion still connecting channels finishes first, so every
|
|
4646
4806
|
// channel it started is in the list stopped below; seeing `stopping`,
|
|
4647
4807
|
// it starts nothing after that.
|
|
@@ -4710,11 +4870,12 @@ export const createGateway = (options = {}) => {
|
|
|
4710
4870
|
// up: the wrapper it would wait for is around the very turn that
|
|
4711
4871
|
// ignored its abort.
|
|
4712
4872
|
const schedulerDrain = scheduler.drain();
|
|
4873
|
+
const schedulerWindow = remaining();
|
|
4713
4874
|
if (drained || abortAfterMs === undefined) {
|
|
4714
4875
|
await schedulerDrain;
|
|
4715
4876
|
}
|
|
4716
|
-
else if (!(await settlesWithin(schedulerDrain,
|
|
4717
|
-
warn(`${kind}: a scheduled firing did not stop after being aborted; shutting down around it`);
|
|
4877
|
+
else if (!(await settlesWithin(schedulerDrain, schedulerWindow))) {
|
|
4878
|
+
warn(`${kind}: a scheduled firing did not stop within ${schedulerWindow}ms after being aborted; shutting down around it`);
|
|
4718
4879
|
}
|
|
4719
4880
|
// After the turns that might still be using them. A plugin holding a
|
|
4720
4881
|
// browser is the reason this exists, and closing it out from under a
|
|
@@ -5064,6 +5225,12 @@ export const createGateway = (options = {}) => {
|
|
|
5064
5225
|
eventDeliveries(options = {}) {
|
|
5065
5226
|
return eventStore?.deliveries(options) ?? [];
|
|
5066
5227
|
},
|
|
5228
|
+
memoryPasses(agentId) {
|
|
5229
|
+
return memoryPassHost.passes(agentId);
|
|
5230
|
+
},
|
|
5231
|
+
memoryPass(agentId, passId) {
|
|
5232
|
+
return memoryPassHost.pass(agentId, passId);
|
|
5233
|
+
},
|
|
5067
5234
|
tools() {
|
|
5068
5235
|
// From the registry, not from the load records: the risk reported is
|
|
5069
5236
|
// the one a call will actually be judged at, floor applied, and a
|