@lazyingart/agintiflow 0.20.202 → 0.20.203

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.
@@ -26,11 +26,31 @@ AGINTI_EVIDENCE_SCOPE_JSON: {"mode":"chat-response","request":"Produce only the
26
26
 
27
27
  or:
28
28
 
29
+ ```text
30
+ AGINTI_EVIDENCE_SCOPE_JSON: {"mode":"host-managed-response","request":"Return the report body; LabCanvas compiles and delivers it."}
31
+ ```
32
+
33
+ or:
34
+
29
35
  ```text
30
36
  AGINTI_EVIDENCE_SCOPE_JSON: {"mode":"task","request":"Create the requested PDF from the supplied evidence."}
31
37
  ```
32
38
 
33
- For `chat-response`, ordinary conversation and routing do not require file or command evidence. For `task`, evidence requirements are inferred only from the exact request, not from surrounding wrapper prose. Artifact requests still require real artifacts.
39
+ For `chat-response`, ordinary conversation and routing do not require file or command evidence. `host-managed-response` is for content-only subtasks such as a LaTeX body, translation, completion audit, or scheduled lesson where LabCanvas owns persistence, compilation, validation, and delivery. For `task`, evidence requirements are inferred only from the exact request, not from surrounding wrapper prose. Artifact requests still require real artifacts.
40
+
41
+ ## Provider Tool-Batch Recovery
42
+
43
+ AgInTi requests one effectful tool call at a time, but some OpenAI-compatible
44
+ providers may still emit a small batch. A bounded batch of otherwise valid
45
+ calls is handled sequentially: AgInTi dispatches only the first call, records
46
+ `tool.batch_deferred`, and asks the model to continue from the resulting state.
47
+ Deferred calls are never executed automatically. Per-tool schema, permission,
48
+ secret, path, and irreversible-action checks remain authoritative.
49
+
50
+ Malformed calls, duplicate IDs, unavailable tools, hidden arguments, or an
51
+ oversized batch still stop with `tool_contract_violation`. A host may resume
52
+ the same saved session with another provider after that categorized stop; it
53
+ must not replay the original request or duplicate earlier side effects.
34
54
 
35
55
  ## Local Context Recovery
36
56
 
@@ -43,4 +63,3 @@ npm run check
43
63
  npm run smoke:context-budget-recovery
44
64
  npm run smoke:truthful-completion
45
65
  ```
46
-
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.202",
3
+ "version": "0.20.203",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
6
6
  "license": "Apache-2.0",
@@ -18,6 +18,7 @@ import { SessionStore } from "../src/session-store.js";
18
18
  import {
19
19
  attachToolContract,
20
20
  createToolContract,
21
+ resolveDispatchableToolCallBatch,
21
22
  safeSequentialToolBatchLimit,
22
23
  toolContractFromResponse,
23
24
  validateToolCallBatch,
@@ -888,6 +889,14 @@ assert(
888
889
  }).ok,
889
890
  "mixed read/write batch unexpectedly passed"
890
891
  );
892
+ const recoveredMixedBatch = resolveDispatchableToolCallBatch(
893
+ mixedReadWriteCalls,
894
+ createToolContract([...safeReadDescriptors, strictWriteDescriptor])
895
+ );
896
+ assert(recoveredMixedBatch.ok, "valid mixed batch could not recover through bounded sequential deferral");
897
+ assert(recoveredMixedBatch.recoveredSequentially, "mixed batch recovery was not recorded");
898
+ assert(recoveredMixedBatch.acceptedToolCalls.length === 1, "mixed batch recovery dispatched more than one call");
899
+ assert(recoveredMixedBatch.deferredToolCalls.length === 1, "mixed batch recovery did not defer the extra call");
891
900
  const oversizedReadCalls = Array.from({ length: 5 }, (_, index) =>
892
901
  contractCall(`read-${index}`, "read_file", { path: `file-${index}.txt` })
893
902
  );
@@ -968,6 +977,9 @@ async function runToolContractCase({
968
977
  allowShellTool = false,
969
978
  toolSurfaceMaxTools = 12,
970
979
  targets = [],
980
+ expectedTargets = [],
981
+ followupToolCalls = null,
982
+ expectSequentialRecovery = false,
971
983
  }) {
972
984
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), `agintiflow-tool-contract-${id}-`));
973
985
  const workspace = path.join(tempRoot, "workspace");
@@ -988,7 +1000,10 @@ async function runToolContractCase({
988
1000
  if (textFallback) {
989
1001
  return { choices: [{ message: { role: "assistant", content: responseText } }] };
990
1002
  }
991
- return assistantWithToolCalls(toolCalls);
1003
+ const selectedToolCalls = Array.isArray(followupToolCalls) && requests.length > 1
1004
+ ? followupToolCalls
1005
+ : toolCalls;
1006
+ return assistantWithToolCalls(selectedToolCalls);
992
1007
  },
993
1008
  },
994
1009
  },
@@ -1082,6 +1097,23 @@ async function runToolContractCase({
1082
1097
  const events = await store.loadEvents();
1083
1098
  const contractFailures = events.filter((event) => event.type === "tool.failed" && event.data?.category === "tool-contract-violation");
1084
1099
  assert(clientFactoryCalls === 1, `${id} did not complete readiness and construct exactly one deterministic client`);
1100
+ if (expectSequentialRecovery) {
1101
+ assert(result.stopped !== true, `${id} stopped instead of completing the recovered sequential batch`);
1102
+ assert(contractFailures.length === 0, `${id} recorded a contract failure for a recoverable valid batch`);
1103
+ assert(
1104
+ events.some((event) => event.type === "tool.batch_deferred" && event.data?.deferredCount === toolCalls.length - 1),
1105
+ `${id} did not record bounded sequential deferral`
1106
+ );
1107
+ for (const target of expectedTargets) {
1108
+ const exists = await fs.access(path.join(workspace, target)).then(() => true).catch(() => false);
1109
+ assert(exists, `${id} did not create expected artifact ${target}`);
1110
+ }
1111
+ for (const target of targets) {
1112
+ const exists = await fs.access(path.join(workspace, target)).then(() => true).catch(() => false);
1113
+ assert(!exists, `${id} dispatched deferred artifact ${target}`);
1114
+ }
1115
+ return { result, requests, events, contractFailures, clientFactoryCalls };
1116
+ }
1085
1117
  assert(
1086
1118
  result.stopped === true && result.reason === "tool_contract_violation",
1087
1119
  `${id} did not stop after one bounded repair: ${JSON.stringify({
@@ -1151,16 +1183,19 @@ assert(
1151
1183
 
1152
1184
  const multiCall = await runToolContractCase({
1153
1185
  id: "native-multi-call",
1154
- goal: "Create multi-one.txt and multi-two.txt.",
1186
+ goal: "Create multi-one.txt containing safe.",
1155
1187
  toolCalls: [
1156
- contractCall("multi-one", "write_file", { path: "multi-one.txt", content: "unsafe", mode: "create" }),
1157
- contractCall("multi-two", "write_file", { path: "multi-two.txt", content: "unsafe", mode: "create" }),
1188
+ contractCall("multi-one", "write_file", { path: "multi-one.txt", content: "safe", mode: "create" }),
1189
+ contractCall("multi-two", "write_file", { path: "multi-two.txt", content: "deferred", mode: "create" }),
1158
1190
  ],
1159
- targets: ["multi-one.txt", "multi-two.txt"],
1191
+ followupToolCalls: [contractCall("finish-recovered", "finish", { result: "Created and verified multi-one.txt." })],
1192
+ expectSequentialRecovery: true,
1193
+ expectedTargets: ["multi-one.txt"],
1194
+ targets: ["multi-two.txt"],
1160
1195
  });
1161
1196
  assert(
1162
- multiCall.contractFailures.every((event) => event.data?.errors?.some((error) => error.code === "TOO_MANY_TOOL_CALLS")),
1163
- "multi-call batch did not enforce the one-call cap"
1197
+ multiCall.events.some((event) => event.type === "tool.completed" && event.data?.toolName === "write_file"),
1198
+ "recoverable multi-call batch did not dispatch its first valid call"
1164
1199
  );
1165
1200
 
1166
1201
  const duplicateId = await runToolContractCase({
@@ -96,9 +96,8 @@ import {
96
96
  } from "./step-budget-controller.js";
97
97
  import { selectExecutionPolicy } from "./execution-policy.js";
98
98
  import {
99
- safeSequentialToolBatchLimit,
99
+ resolveDispatchableToolCallBatch,
100
100
  toolContractFromResponse,
101
- validateToolCallBatch,
102
101
  } from "./tool-contract.js";
103
102
  import {
104
103
  createContextBudgetState,
@@ -3909,17 +3908,18 @@ export async function runAgent(config) {
3909
3908
  }
3910
3909
 
3911
3910
  const rawToolCalls = assistantMessage.tool_calls;
3912
- const toolCalls = Array.isArray(rawToolCalls) ? rawToolCalls : [];
3911
+ const reportedToolCalls = Array.isArray(rawToolCalls) ? rawToolCalls : [];
3913
3912
  const toolBatchValidation = rawToolCalls === undefined || rawToolCalls === null
3914
- ? { ok: true, calls: [] }
3915
- : validateToolCallBatch(rawToolCalls, toolContractFromResponse(response), {
3916
- maxToolCalls: safeSequentialToolBatchLimit(rawToolCalls),
3917
- });
3913
+ ? { ok: true, calls: [], acceptedToolCalls: [], deferredToolCalls: [] }
3914
+ : resolveDispatchableToolCallBatch(rawToolCalls, toolContractFromResponse(response));
3915
+ const toolCalls = toolBatchValidation.ok
3916
+ ? (toolBatchValidation.acceptedToolCalls || reportedToolCalls)
3917
+ : reportedToolCalls;
3918
3918
 
3919
3919
  await store.appendEvent("model.responded", {
3920
3920
  step,
3921
3921
  content: redactSensitiveText(assistantMessage.content || ""),
3922
- toolCalls: toolCalls.map((call) => ({
3922
+ toolCalls: reportedToolCalls.map((call) => ({
3923
3923
  id: call?.id,
3924
3924
  name: call?.function?.name,
3925
3925
  arguments: redactSensitiveText(call?.function?.arguments || ""),
@@ -3962,7 +3962,27 @@ export async function runAgent(config) {
3962
3962
  continue;
3963
3963
  }
3964
3964
 
3965
- state.messages.push(preserveAssistantMessage(assistantMessage));
3965
+ if (toolBatchValidation.recoveredSequentially) {
3966
+ const deferredToolCalls = toolBatchValidation.deferredToolCalls || [];
3967
+ const detail = {
3968
+ step,
3969
+ reportedCount: reportedToolCalls.length,
3970
+ dispatchedCount: toolCalls.length,
3971
+ deferredCount: deferredToolCalls.length,
3972
+ dispatchedTool: String(toolCalls[0]?.function?.name || ""),
3973
+ deferredTools: deferredToolCalls.map((call) => String(call?.function?.name || "")).filter(Boolean),
3974
+ };
3975
+ await store.appendEvent("tool.batch_deferred", detail);
3976
+ observers.event("tool.batch_deferred", detail);
3977
+ }
3978
+
3979
+ state.messages.push(
3980
+ preserveAssistantMessage(
3981
+ toolBatchValidation.recoveredSequentially
3982
+ ? { ...assistantMessage, tool_calls: toolCalls }
3983
+ : assistantMessage
3984
+ )
3985
+ );
3966
3986
 
3967
3987
  if (toolCalls.length === 0) {
3968
3988
  const queuedCount = await injectQueuedUserMessages(store, state, observers);
@@ -519,7 +519,7 @@ function scopedChatopsEvidenceGoal(goal = "", taskProfile = "") {
519
519
  const payload = JSON.parse(match[1]);
520
520
  if (!payload || typeof payload !== "object") return String(goal || "");
521
521
  const mode = String(payload.mode || "").trim().toLowerCase();
522
- if (["chat-response", "plan-response", "read-only-answer"].includes(mode)) {
522
+ if (["chat-response", "host-managed-response", "plan-response", "read-only-answer"].includes(mode)) {
523
523
  return "Answer the current chat turn directly without external execution.";
524
524
  }
525
525
  const request = String(payload.request || "").trim();
@@ -29,6 +29,7 @@ const SAFE_SEQUENTIAL_READ_TOOLS = new Set([
29
29
  const MAX_VALIDATION_ERRORS = 8;
30
30
  const MAX_VALIDATION_NODES = 50_000;
31
31
  const MAX_SAFE_SEQUENTIAL_READ_CALLS = 4;
32
+ const MAX_RECOVERABLE_SEQUENTIAL_CALLS = 4;
32
33
 
33
34
  function cloneValue(value) {
34
35
  return structuredClone(value);
@@ -331,3 +332,42 @@ export function validateToolCallBatch(toolCalls, contract, { maxToolCalls = 1 }
331
332
  }
332
333
  return { ok: true, calls: parsedCalls };
333
334
  }
335
+
336
+ export function resolveDispatchableToolCallBatch(toolCalls, contract) {
337
+ const calls = Array.isArray(toolCalls) ? toolCalls : [];
338
+ const validation = validateToolCallBatch(calls, contract, {
339
+ maxToolCalls: safeSequentialToolBatchLimit(calls),
340
+ });
341
+ if (validation.ok) {
342
+ return {
343
+ ...validation,
344
+ acceptedToolCalls: calls,
345
+ deferredToolCalls: [],
346
+ recoveredSequentially: false,
347
+ };
348
+ }
349
+
350
+ const errors = Array.isArray(validation.errors) ? validation.errors : [];
351
+ const onlyExceededBatchLimit =
352
+ errors.length > 0 && errors.every((error) => error?.code === "TOO_MANY_TOOL_CALLS");
353
+ if (
354
+ !onlyExceededBatchLimit ||
355
+ calls.length <= 1 ||
356
+ calls.length > MAX_RECOVERABLE_SEQUENTIAL_CALLS
357
+ ) {
358
+ return validation;
359
+ }
360
+
361
+ const firstCallValidation = validateToolCallBatch([calls[0]], contract, {
362
+ maxToolCalls: 1,
363
+ });
364
+ if (!firstCallValidation.ok) return validation;
365
+
366
+ return {
367
+ ...firstCallValidation,
368
+ acceptedToolCalls: [calls[0]],
369
+ deferredToolCalls: calls.slice(1),
370
+ recoveredSequentially: true,
371
+ originalCode: validation.code,
372
+ };
373
+ }