@dsh-enhanced/assistant-growth-driver 0.1.66 → 0.1.67

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.
@@ -4,7 +4,7 @@ import { anySourceGrowthRunDigest, validateWorkItemSourceGrowthRunBinding, sourc
4
4
  import { installModelSelection } from '@deepseek-ai/dsh-agent';
5
5
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
6
6
  import { SessionId } from '@deepseek-ai/dsh-session';
7
- import { defineTool } from '@deepseek-ai/dsh-tools';
7
+ import { defineTool, RUN_CODE_NAME } from '@deepseek-ai/dsh-tools';
8
8
  import { workItemRunSource } from './work-item-source.js';
9
9
  import { GROWTH_PROTECTED_PLUGIN_DENYLIST, } from './source-port.js';
10
10
  import { resolveSourcePreparation } from './source-edits.js';
@@ -872,7 +872,131 @@ function registerGrowthTools(agent, input, sourcePlane, creationNamespace, revis
872
872
  }
873
873
  agentCtx.effect(() => () => disposers.forEach(dispose => dispose()), 'assistant-growth-driver.realm-tools');
874
874
  }
875
- function summarize(events, signal, modelCalls, toolCalls) {
875
+ /** Extract only the program source from one raw `tool/call` arguments JSON string. */
876
+ function runCodeSource(raw) {
877
+ if (typeof raw !== 'string')
878
+ return undefined;
879
+ try {
880
+ const parsed = JSON.parse(raw);
881
+ return typeof parsed.code === 'string' ? parsed.code : undefined;
882
+ }
883
+ catch {
884
+ return undefined;
885
+ }
886
+ }
887
+ const eventId = (value) => typeof value === 'string' ? value : typeof value === 'number' && Number.isFinite(value) ? String(value) : undefined;
888
+ /**
889
+ * Project a bounded structural trajectory from the session's PTC log events.
890
+ * Deliberately reads only names, ids, times, error flags and byte lengths: the
891
+ * nested `arguments` and `content` carried by `tool/ptc-dispatch`/`-start`
892
+ * and the program source in `tool/call` never survive into the returned
893
+ * object, and unknown/malformed records are skipped rather than trusted.
894
+ * Bounded by the frozen tool allowlist and by plain integer counters.
895
+ */
896
+ export function ptcTrajectory(events, admittedToolNames) {
897
+ let runCodeCalls = 0, failedRunCodeCalls = 0, scriptsHashed = 0, scriptBytes = 0;
898
+ let nestedCalls = 0, failedNestedCalls = 0, unadmittedNestedCalls = 0;
899
+ let longestSubCallMs = 0, longestRunCodeMs = 0;
900
+ const scriptHash = createHash('sha256');
901
+ const runCodeStarts = new Map();
902
+ const subCallStarts = new Map();
903
+ const byName = new Map();
904
+ const timeOf = (event) => typeof event.time === 'number' && Number.isFinite(event.time) ? event.time : undefined;
905
+ for (const raw of events) {
906
+ if (raw === null || typeof raw !== 'object')
907
+ continue;
908
+ const data = raw.data;
909
+ if (data === undefined || data === null || typeof data !== 'object')
910
+ continue;
911
+ const time = timeOf(raw);
912
+ if (raw.type === 'tool/call') {
913
+ if (data.name !== RUN_CODE_NAME)
914
+ continue;
915
+ runCodeCalls += 1;
916
+ const callId = eventId(data.callId);
917
+ if (callId !== undefined && time !== undefined)
918
+ runCodeStarts.set(callId, time);
919
+ const code = runCodeSource(data.arguments);
920
+ if (code !== undefined) {
921
+ const bytes = Buffer.byteLength(code, 'utf8');
922
+ if (Number.isSafeInteger(scriptBytes + bytes))
923
+ scriptBytes += bytes;
924
+ // Length-prefix keeps the aggregate digest unambiguous across scripts.
925
+ scriptHash.update(`${bytes}\0`);
926
+ scriptHash.update(code);
927
+ scriptsHashed += 1;
928
+ }
929
+ continue;
930
+ }
931
+ if (raw.type === 'tool/result') {
932
+ // ToolResultMessage carries the call identity as `toolCallId`; older
933
+ // projections used `callId`, so accept either without trusting anything
934
+ // else from the message.
935
+ const message = data.message;
936
+ if (message === undefined || message === null || typeof message !== 'object')
937
+ continue;
938
+ const callId = eventId(message.toolCallId) ?? eventId(message.callId);
939
+ const start = callId === undefined ? undefined : runCodeStarts.get(callId);
940
+ if (callId === undefined || start === undefined)
941
+ continue;
942
+ runCodeStarts.delete(callId);
943
+ if (message.isError === true)
944
+ failedRunCodeCalls += 1;
945
+ if (time !== undefined)
946
+ longestRunCodeMs = Math.max(longestRunCodeMs, Math.max(0, time - start));
947
+ continue;
948
+ }
949
+ if (raw.type === 'tool/ptc-dispatch-start') {
950
+ const subCallId = eventId(data.subCallId);
951
+ if (subCallId !== undefined && time !== undefined)
952
+ subCallStarts.set(subCallId, time);
953
+ continue;
954
+ }
955
+ if (raw.type === 'tool/ptc-dispatch') {
956
+ nestedCalls += 1;
957
+ const name = typeof data.name === 'string' ? data.name : undefined;
958
+ if (name !== undefined && admittedToolNames.has(name))
959
+ byName.set(name, (byName.get(name) ?? 0) + 1);
960
+ else
961
+ unadmittedNestedCalls += 1;
962
+ if (data.isError === true)
963
+ failedNestedCalls += 1;
964
+ const subCallId = eventId(data.subCallId);
965
+ const start = subCallId === undefined ? undefined : subCallStarts.get(subCallId);
966
+ if (subCallId !== undefined && start !== undefined) {
967
+ subCallStarts.delete(subCallId);
968
+ if (time !== undefined)
969
+ longestSubCallMs = Math.max(longestSubCallMs, Math.max(0, time - start));
970
+ }
971
+ }
972
+ }
973
+ return Object.freeze({
974
+ protocol: 'assistant-growth/ptc-trajectory/v1',
975
+ runCodeCalls, failedRunCodeCalls, scriptBytes,
976
+ scriptDigest: scriptsHashed > 0 ? scriptHash.digest('hex') : null,
977
+ nestedCalls, failedNestedCalls, unadmittedNestedCalls,
978
+ toolCallsByName: Object.freeze(Object.fromEntries([...byName.entries()].sort(([left], [right]) => left.localeCompare(right)))),
979
+ longestSubCallMs, longestRunCodeMs,
980
+ });
981
+ }
982
+ /**
983
+ * Split the registry's presentation-agnostic visibility view into the frozen
984
+ * allowlist surface and the reserved PTC transport. `schemas(scope)` stays
985
+ * presentation-agnostic for everything the growth agent registered, but under
986
+ * `ptc` the Host registry appends its own reserved `run_code` definition.
987
+ * Keeping the split in one pure helper means the contract digest can never
988
+ * accidentally include the presentation transport (native and PTC runs of the
989
+ * same configuration persist the identical tool contract digest) while the
990
+ * fail-closed surface check can still require exactly one reserved transport.
991
+ */
992
+ export function splitGrowthToolSurface(schemas) {
993
+ const surface = [];
994
+ const reserved = [];
995
+ for (const schema of schemas)
996
+ (schema.name === RUN_CODE_NAME ? reserved : surface).push(schema);
997
+ return { surface, reserved };
998
+ }
999
+ function summarize(events, signal, modelCalls, toolCalls, presentation, admittedToolNames) {
876
1000
  let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0, reasoningTokens = 0;
877
1001
  let output = '';
878
1002
  let outcome = 'unknown';
@@ -912,7 +1036,10 @@ function summarize(events, signal, modelCalls, toolCalls) {
912
1036
  outcome = 'failed';
913
1037
  }
914
1038
  }
915
- return { outcome, output, usage: { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, modelCalls, toolCalls } };
1039
+ return { outcome, output, usage: { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, reasoningTokens, modelCalls, toolCalls,
1040
+ // Native wakes keep exactly the previous usage shape; the PTC block is
1041
+ // opt-in and structural only.
1042
+ ...(presentation === 'ptc' ? { ptc: ptcTrajectory(events, admittedToolNames) } : {}) } };
916
1043
  }
917
1044
  /**
918
1045
  * Create, run to idle and dispose exactly one bounded growth agent. Never
@@ -924,6 +1051,25 @@ export async function runGrowthAgent(ctx, input) {
924
1051
  if (workItemSource)
925
1052
  input = { ...input, config: { ...input.config, pluginSourceProposals: { ...input.config.pluginSourceProposals, allowModification: false, allowRevision: false } } };
926
1053
  const { authority, config, model } = input;
1054
+ const presentation = config.toolPresentation;
1055
+ if (presentation === 'ptc') {
1056
+ // The WorkItem author lane freezes a four-tool preauthorization set and its
1057
+ // own execution contract; the reserved transport is not part of it, so a
1058
+ // PTC wake there would fail mid-run with a misleading authorization error.
1059
+ // Reject the combination before any Agent is created and tell the owner why.
1060
+ if (workItemSource) {
1061
+ throw new Error('assistant-growth-driver: PTC presentation is not supported for the WorkItem author lane yet: '
1062
+ + 'the reserved run_code transport is not in its frozen preauthorization; '
1063
+ + 'use toolPresentation "native" for workItemFailures or wait for a Host-owned preauthorization');
1064
+ }
1065
+ // Fail closed and loudly, never fall back to native: the model surface is
1066
+ // an owner-declared contract, and a profile that opted into PTC but does not
1067
+ // mount the runtime seam cannot honestly honor it. No Agent is created.
1068
+ if (ctx.get('ptcRuntime', false) === undefined) {
1069
+ throw new Error('assistant-growth-driver: toolPresentation "ptc" requires a mounted ctx.ptcRuntime implementation '
1070
+ + '(e.g. @deepseek-ai/dsh-ptc-runtime-node); load a PTC runtime in this profile or set toolPresentation to "native"');
1071
+ }
1072
+ }
927
1073
  const sourcePlane = config.pluginSourceProposals.enabled ? input.sourcePlane : undefined;
928
1074
  let creationNamespace;
929
1075
  if (sourcePlane !== undefined && config.pluginSourceProposals.allowCreation
@@ -1089,12 +1235,44 @@ export async function runGrowthAgent(ctx, input) {
1089
1235
  const denied = globalNames.filter(name => !allowedTools.has(name));
1090
1236
  if (denied.length > 0)
1091
1237
  agentCtx.tools.restrict({ deny: denied });
1092
- const finalNames = agentCtx.tools.schemas(agent).map(schema => schema.name);
1093
- const outsideAllowlist = finalNames.filter(name => !allowedTools.has(name));
1094
- if (outsideAllowlist.length > 0 || finalNames.length !== allowedTools.size) {
1095
- throw new Error(`assistant-growth-driver: growth Agent tool surface is not exactly the frozen allowlist: ${outsideAllowlist.join(', ')}`);
1238
+ if (presentation === 'ptc') {
1239
+ // PTC is presentation only. The single reserved `run_code` transport is
1240
+ // appended by the Host registry through this scoped presentAs() effect
1241
+ // (an effect owned by the Agent realm, released with it); the driver
1242
+ // never registers, wraps or overrides that reserved name. Refuse any
1243
+ // pre-existing definition before the declaration so a forged or
1244
+ // duplicate transport can never be mistaken for the Host one.
1245
+ if (typeof agentCtx.tools.presentAs !== 'function') {
1246
+ throw new Error('assistant-growth-driver: toolPresentation "ptc" requires a Host tool registry with presentAs()');
1247
+ }
1248
+ if (agentCtx.tools.schemas(agent).some(schema => schema.name === RUN_CODE_NAME)) {
1249
+ throw new Error('assistant-growth-driver: reserved run_code transport is already visible before PTC presentation');
1250
+ }
1251
+ agentCtx.tools.presentAs('ptc');
1252
+ const reserved = agentCtx.tools.get(RUN_CODE_NAME, agent);
1253
+ if (reserved === undefined || reserved.name !== RUN_CODE_NAME || agentCtx.tools.get(RUN_CODE_NAME, agent) !== reserved) {
1254
+ throw new Error('assistant-growth-driver: PTC growth Agent does not expose the Host reserved run_code transport');
1255
+ }
1256
+ }
1257
+ const finalSchemas = agentCtx.tools.schemas(agent);
1258
+ const { surface: allowlistedSchemas, reserved: reservedSchemas } = splitGrowthToolSurface(finalSchemas);
1259
+ const outsideAllowlist = allowlistedSchemas.filter(schema => !allowedTools.has(schema.name)).map(schema => schema.name);
1260
+ const missing = [...allowedTools].filter(name => !allowlistedSchemas.some(schema => schema.name === name));
1261
+ // Exactly the frozen allowlist plus, only under PTC, exactly one reserved
1262
+ // transport definition. Missing, extra, duplicated or foreign entries all
1263
+ // fail closed before any model request.
1264
+ if (outsideAllowlist.length > 0 || missing.length > 0
1265
+ || reservedSchemas.length !== (presentation === 'ptc' ? 1 : 0)
1266
+ || reservedSchemas.some(schema => schema.name !== RUN_CODE_NAME)) {
1267
+ throw new Error(`assistant-growth-driver: growth Agent tool surface is not exactly the frozen allowlist: ${[...outsideAllowlist, ...missing].join(', ')}`);
1096
1268
  }
1097
- const pinnedDigest = acceptanceDigest(agentCtx.tools.schemas(agent).sort((a, b) => a.name.localeCompare(b.name)));
1269
+ // Contract digest: allowlist-only, so the same configuration persists the
1270
+ // identical tool contract digest under native and PTC presentation.
1271
+ const pinnedDigest = acceptanceDigest([...allowlistedSchemas].sort((a, b) => a.name.localeCompare(b.name)));
1272
+ // Model-visible wire digest, deliberately separate from pinnedDigest:
1273
+ // native presents the allowlist surface, PTC presents only the reserved
1274
+ // transport. Recomputed from the filtered assembly before every request.
1275
+ let wireDigest;
1098
1276
  const session = agent.session;
1099
1277
  const definitions = new Map();
1100
1278
  let binding;
@@ -1104,7 +1282,9 @@ export async function runGrowthAgent(ctx, input) {
1104
1282
  || agent.session !== session || String(session.id) !== String(sessionId) || agent.ctx !== agentCtx
1105
1283
  || session.header.cwd !== authority.scope.workspace || session.header.agentPreset !== authority.scope.preset
1106
1284
  || preauthorizationRemovers.length !== 4 || anySourceGrowthRunDigest(binding) !== bindingDigest
1107
- || acceptanceDigest(agentCtx.tools.schemas(agent).sort((a, b) => a.name.localeCompare(b.name))) !== pinnedDigest
1285
+ // Digest the allowlist surface only: the PTC presentation transport is
1286
+ // never part of the frozen tool contract.
1287
+ || acceptanceDigest([...splitGrowthToolSurface(agentCtx.tools.schemas(agent)).surface].sort((a, b) => a.name.localeCompare(b.name))) !== pinnedDigest
1108
1288
  || WORK_ITEM_SOURCE_TOOL_NAMES.some(name => agentCtx.tools.get(name, agent) !== definitions.get(name)))
1109
1289
  return false;
1110
1290
  if (!execution)
@@ -1146,7 +1326,19 @@ export async function runGrowthAgent(ctx, input) {
1146
1326
  const assembly = await next();
1147
1327
  if (context.agent !== agent)
1148
1328
  return assembly;
1149
- return { ...assembly, tools: assembly.tools.filter(tool => allowedTools.has(tool.name)).sort((a, b) => a.name.localeCompare(b.name)) };
1329
+ if (presentation === 'ptc') {
1330
+ // The Host assembles the collapsed PTC wire surface; anything other
1331
+ // than exactly the reserved transport means the presentation did not
1332
+ // take effect and must fail closed, never leak the native surface.
1333
+ if (assembly.tools.length !== 1 || assembly.tools[0]?.name !== RUN_CODE_NAME) {
1334
+ throw new Error('assistant-growth-driver: PTC growth Agent must present exactly the reserved run_code transport');
1335
+ }
1336
+ wireDigest = acceptanceDigest(assembly.tools);
1337
+ return { ...assembly, tools: assembly.tools };
1338
+ }
1339
+ const tools = assembly.tools.filter(tool => allowedTools.has(tool.name)).sort((a, b) => a.name.localeCompare(b.name));
1340
+ wireDigest = acceptanceDigest(tools);
1341
+ return { ...assembly, tools };
1150
1342
  });
1151
1343
  agentCtx.on('llm/stream', async function* (options, next) {
1152
1344
  if (options.sessionId !== agent.session.id) {
@@ -1161,7 +1353,9 @@ export async function runGrowthAgent(ctx, input) {
1161
1353
  if (options.provider !== model.provider || options.model !== model.model
1162
1354
  || model.reasoningEffort !== undefined && options.reasoningEffort !== model.reasoningEffort
1163
1355
  || options.maxTokens === undefined || options.maxTokens > config.maxOutputTokens
1164
- || acceptanceDigest(options.tools ?? []) !== pinnedDigest
1356
+ // The wire digest is pinned separately from the allowlist contract
1357
+ // digest: under PTC the request carries only the reserved transport.
1358
+ || wireDigest === undefined || acceptanceDigest(options.tools ?? []) !== wireDigest
1165
1359
  || modelCalls >= config.maxModelCalls) {
1166
1360
  agent.cancel({ kind: 'hook', reason: 'assistant-growth-driver-model-rejected' });
1167
1361
  throw new Error('assistant-growth-driver: growth model request rejected by the frozen contract');
@@ -1191,7 +1385,14 @@ export async function runGrowthAgent(ctx, input) {
1191
1385
  throw error;
1192
1386
  }
1193
1387
  combined.throwIfAborted();
1194
- if (!allowedTools.has(execution.name) || totalToolCalls >= config.maxToolCalls) {
1388
+ // The reserved PTC transport is the ONLY name outside the frozen
1389
+ // allowlist that may execute, and only under the declared PTC mode.
1390
+ // Every execution that reaches this guard - model-direct `run_code`
1391
+ // and each nested sub-dispatch, failed or cancelled included after
1392
+ // admission - counts against the same maxToolCalls budget.
1393
+ const admitted = allowedTools.has(execution.name)
1394
+ || (presentation === 'ptc' && execution.name === RUN_CODE_NAME);
1395
+ if (!admitted || totalToolCalls >= config.maxToolCalls) {
1195
1396
  agent.cancel({ kind: 'hook', reason: 'assistant-growth-driver-tool-limit' });
1196
1397
  return 'assistant-growth-driver: tool request rejected by the frozen allowlist';
1197
1398
  }
@@ -1238,6 +1439,10 @@ export async function runGrowthAgent(ctx, input) {
1238
1439
  executionContractDigest: acceptanceDigest({
1239
1440
  protocol: 'assistant-growth/execution-contract/v1',
1240
1441
  prompt: executionPrompt,
1442
+ // Native keeps the historical contract shape byte-for-byte; PTC
1443
+ // additionally freezes which presentation the wake was authorized
1444
+ // for. The model surface difference can never be silent.
1445
+ ...(presentation === 'ptc' ? { toolPresentation: 'ptc' } : {}),
1241
1446
  guardVersion: 1, toolContractDigest: pinnedDigest, model,
1242
1447
  bounds: { maxModelCalls: config.maxModelCalls, maxToolCalls: config.maxToolCalls,
1243
1448
  maxOutputTokens: config.maxOutputTokens, maxDurationMs: config.maxDurationMs,
@@ -1279,7 +1484,7 @@ export async function runGrowthAgent(ctx, input) {
1279
1484
  source: { kind: 'assistant-growth-driver', form: 'notice', summary: 'Growth review wake' },
1280
1485
  }));
1281
1486
  await agent.whenIdle();
1282
- const summary = summarize(agent.session.snapshotEvents(), combined, modelCalls, totalToolCalls);
1487
+ const summary = summarize(agent.session.snapshotEvents(), combined, modelCalls, totalToolCalls, presentation, allowedTools);
1283
1488
  await sessions.flush(agent.session);
1284
1489
  return { sessionId: String(sessionId), model, ...summary, sourceProposals: Object.freeze({ ...sourceCounters }) };
1285
1490
  }