@lazyingart/agintiflow 0.20.328 → 0.20.330

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.
@@ -675,3 +675,49 @@ demonstrated evidence from inferred recommendations, and finished normally.
675
675
  The retained trace contains no completion-evidence rejection or forced
676
676
  source-mutation loop. Focused completion, evidence, progressive-tool, runtime,
677
677
  and dynamic-budget suites plus the full npm suite pass for `0.20.303`.
678
+
679
+ ### Response-only context compaction after provider handoff
680
+
681
+ `response-only-context-handoff-092` exercises a high-risk continuation boundary
682
+ for DeepSeek-first operation with LocalLLM fallback. Recent same-session
683
+ response-only evidence showed DeepSeek quota failures followed by LocalLLM
684
+ resume attempts that failed before inference because the retained transcript
685
+ exceeded the LocalLLM context window. Normal agent-step requests already had a
686
+ local context-budget compaction retry, but the explicit response-only branch
687
+ called the direct-response client without that recovery path.
688
+
689
+ AgInTiFlow now catches only `LOCALLLM_CONTEXT_BUDGET_EXCEEDED` in the
690
+ response-only branch, compacts the authoritative retained goal/evidence once,
691
+ persists `model.local_context_budget_exceeded` and
692
+ `history.compacted_for_local_context_retry`, and retries the same response-only
693
+ request with a bounded output reserve. The source-free evidence guard remains
694
+ active after compaction, so unsupported publication, validation, forecast,
695
+ benchmark, or metric claims still retry once and then fail closed.
696
+
697
+ The focused regression seeds a DeepSeek-owned response-only session, inflates
698
+ retained same-session context, resumes with a normal "answer from the saved
699
+ status" prompt, triggers a DeepSeek quota handoff, and verifies that LocalLLM is
700
+ called only after compaction. The run completes without `session.failed`, keeps
701
+ the provider handoff active on the same session, and records the compaction
702
+ events as durable evidence.
703
+
704
+ ### Harmless tool-call annotations during repository discovery
705
+
706
+ `inspect-project-annotation-093` covers a DeepSeek-first tool-loop boundary
707
+ that was still under-tested after the provider handoff work. Recent runtime
708
+ evidence showed ordinary project-inspection tasks stopping with
709
+ `tool_contract_violation` before dispatch because the model included a
710
+ non-executable `reason` field in an otherwise valid `inspect_project` call.
711
+ The strict per-turn schema correctly rejected unknown executable fields, but
712
+ the existing benign-annotation normalizer only recognized `description`.
713
+
714
+ AgInTiFlow now treats a bounded string `reason` exactly like `description`: it
715
+ is removed before schema validation only when the offered tool schema forbids
716
+ additional properties and the schema does not define that field. Structured,
717
+ non-string, oversized, or executable unknown fields still fail closed.
718
+
719
+ The regression uses a normal weak prompt asking the agent to look over a small
720
+ repository and report whether a README exists. The scripted DeepSeek-shaped
721
+ response calls `inspect_project` with `reason` plus a real `limit`; the persisted
722
+ runtime dispatches the inspection, records zero tool-contract failures, and
723
+ finishes with the verified README status without mutating the workspace.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.328",
3
+ "version": "0.20.330",
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",
@@ -7238,6 +7238,54 @@ assert(
7238
7238
  ).ok,
7239
7239
  "valid offered tool call did not satisfy its exact schema"
7240
7240
  );
7241
+ const strictInspectDescriptor = {
7242
+ type: "function",
7243
+ function: {
7244
+ name: "inspect_project",
7245
+ description: "Inspect the project.",
7246
+ parameters: {
7247
+ type: "object",
7248
+ properties: {
7249
+ path: { type: "string" },
7250
+ maxDepth: { type: "integer" },
7251
+ limit: { type: "integer" },
7252
+ includeFiles: { type: "boolean" },
7253
+ },
7254
+ additionalProperties: false,
7255
+ },
7256
+ },
7257
+ };
7258
+ const recoveredInspectReason = resolveDispatchableToolCallBatch(
7259
+ [
7260
+ contractCall("inspect-with-reason", "inspect_project", {
7261
+ reason: "Orient before answering the user's project question.",
7262
+ limit: 120,
7263
+ }),
7264
+ ],
7265
+ createToolContract([strictInspectDescriptor])
7266
+ );
7267
+ assert(
7268
+ recoveredInspectReason.ok && recoveredInspectReason.recoveredToolCallAnnotations,
7269
+ "a harmless inspect_project reason annotation was not stripped before schema validation"
7270
+ );
7271
+ assertStrict.deepEqual(
7272
+ JSON.parse(recoveredInspectReason.acceptedToolCalls[0].function.arguments),
7273
+ { limit: 120 },
7274
+ "inspect_project reason annotation recovery changed executable arguments"
7275
+ );
7276
+ const rejectedStructuredInspectReason = resolveDispatchableToolCallBatch(
7277
+ [
7278
+ contractCall("inspect-structured-reason", "inspect_project", {
7279
+ reason: { intent: "orient" },
7280
+ limit: 120,
7281
+ }),
7282
+ ],
7283
+ createToolContract([strictInspectDescriptor])
7284
+ );
7285
+ assert(
7286
+ !rejectedStructuredInspectReason.ok,
7287
+ "a structured inspect_project reason annotation was stripped even though it is not a bounded text note"
7288
+ );
7241
7289
 
7242
7290
  const safeReadDescriptors = [
7243
7291
  {
@@ -7877,6 +7925,73 @@ assert(
7877
7925
  "authoritative routine flow still dispatched private/raw exploratory commands"
7878
7926
  );
7879
7927
 
7928
+ let annotatedInspectStep = 0;
7929
+ const annotatedInspectRun = await runToolContractCase({
7930
+ id: "inspect-project-reason-annotation",
7931
+ provider: "deepseek",
7932
+ profile: "code",
7933
+ goal: "Please look over this little repo and tell me whether it has a README. Keep it short.",
7934
+ toolCalls: [
7935
+ contractCall("unused-annotated-inspect", "finish", { result: "unused" }),
7936
+ ],
7937
+ expectSuccess: true,
7938
+ expectedContractFailures: 0,
7939
+ setupWorkspace: async (workspace) => {
7940
+ await fs.writeFile(
7941
+ path.join(workspace, "README.md"),
7942
+ "# Demo\nThis workspace has a README.\n",
7943
+ "utf8"
7944
+ );
7945
+ },
7946
+ responseFactory: ({ payload }) => {
7947
+ annotatedInspectStep += 1;
7948
+ const offered = Array.isArray(payload.tools) ? names(payload.tools) : [];
7949
+ if (annotatedInspectStep === 1) {
7950
+ assert(
7951
+ offered.includes("inspect_project"),
7952
+ "normal repository orientation did not offer inspect_project"
7953
+ );
7954
+ return assistantWithToolCalls([
7955
+ contractCall("inspect-with-natural-reason", "inspect_project", {
7956
+ reason: "Orient before answering the user's project question.",
7957
+ limit: 120,
7958
+ }),
7959
+ ]);
7960
+ }
7961
+ return assistantWithToolCalls([
7962
+ contractCall("finish-after-annotated-inspect", "finish", {
7963
+ result: "README.md is present.",
7964
+ }),
7965
+ ]);
7966
+ },
7967
+ });
7968
+ assertStrict.equal(
7969
+ annotatedInspectRun.requests.length,
7970
+ 2,
7971
+ "annotated inspect_project recovery used extra model turns"
7972
+ );
7973
+ assert(
7974
+ annotatedInspectRun.events.some(
7975
+ (event) =>
7976
+ event.type === "tool.completed" &&
7977
+ event.data?.toolName === "inspect_project" &&
7978
+ event.data?.ok === true
7979
+ ),
7980
+ "annotated inspect_project call was not dispatched through the real runtime"
7981
+ );
7982
+ assert(
7983
+ !annotatedInspectRun.events.some(
7984
+ (event) =>
7985
+ event.type === "tool.failed" &&
7986
+ event.data?.category === "tool-contract-violation"
7987
+ ),
7988
+ "annotated inspect_project still produced a tool-contract violation"
7989
+ );
7990
+ assert(
7991
+ /README\.md is present/.test(String(annotatedInspectRun.result?.result || "")),
7992
+ "annotated inspect_project run did not finish with the promised repository status"
7993
+ );
7994
+
7880
7995
  const hiddenDryRun = await runToolContractCase({
7881
7996
  id: "native-hidden-dry-run",
7882
7997
  goal: "Create hidden-dry-run.txt containing unsafe if this tool dispatches.",
@@ -4155,6 +4155,72 @@ function responseOnlySourceFreeStopResult(assessment = {}) {
4155
4155
  }
4156
4156
 
4157
4157
  async function finishWithResponseOnlyModelTurn({ client, config, state, store, observers, sessionId }) {
4158
+ async function requestWithResponseOnlyLocalContextRecovery({ step, mode }) {
4159
+ try {
4160
+ return await requestDirectResponse(client, config, state.messages);
4161
+ } catch (error) {
4162
+ if (!isLocalContextBudgetError(error)) throw error;
4163
+ state.meta = state.meta || {};
4164
+ const retried = state.meta.localContextBudgetRetries || {};
4165
+ const retryKey = `response-only:${mode}`;
4166
+ if (retried[retryKey]) throw error;
4167
+
4168
+ const requestMessages = Array.isArray(state.messages) ? state.messages : [];
4169
+ const currentOutputTokens = Math.max(0, Number(config.maxOutputTokens || 0));
4170
+ const retryOutputTokens = currentOutputTokens
4171
+ ? Math.min(currentOutputTokens, 4096)
4172
+ : 4096;
4173
+ const retryRuntimeConfig = {
4174
+ ...config,
4175
+ maxOutputTokens: retryOutputTokens,
4176
+ };
4177
+ const compactMessages = buildContextBudgetCompactionMessages(
4178
+ state,
4179
+ retryRuntimeConfig,
4180
+ null,
4181
+ step,
4182
+ {
4183
+ heading: "A response-only LocalLLM request exceeded the local context window.",
4184
+ detail: redactSensitiveText(error instanceof Error ? error.message : String(error)),
4185
+ recoveryInstruction:
4186
+ "Answer the current response-only request directly from the compacted authoritative context above. Do not claim fresh external evidence unless the compacted context contains a current evidence manifest or scoped tool evidence.",
4187
+ }
4188
+ );
4189
+ const detail = {
4190
+ step,
4191
+ mode,
4192
+ provider: config.provider,
4193
+ model: config.model,
4194
+ messageCharsBefore: countMessageChars(requestMessages),
4195
+ messageCharsAfter: countMessageChars(compactMessages),
4196
+ messageTokensBefore: estimateMessageTokens(requestMessages),
4197
+ messageTokensAfter: estimateMessageTokens(compactMessages),
4198
+ maxOutputTokens: retryOutputTokens,
4199
+ error: redactSensitiveText(error instanceof Error ? error.message : String(error)),
4200
+ };
4201
+ state.messages = compactMessages;
4202
+ resetStaticDiscoveryAfterContextLoss(state, "response-only-local-context-budget-retry", {
4203
+ preserveStaticEvidence: true,
4204
+ });
4205
+ state.meta.localContextBudgetRetries = {
4206
+ ...retried,
4207
+ [retryKey]: true,
4208
+ };
4209
+ state.meta.lastResponseOnlyContextBudgetRecovery = detail;
4210
+ await store.appendEvent("model.local_context_budget_exceeded", detail);
4211
+ await store.appendEvent("history.compacted_for_local_context_retry", detail);
4212
+ observers.event("model.local_context_budget_exceeded", detail);
4213
+ observers.event("history.compacted_for_local_context_retry", detail);
4214
+ emitConsole(
4215
+ config,
4216
+ "Local provider context exceeded its configured window for a response-only turn; compacted authoritative context and retrying once.",
4217
+ { kind: "meta" }
4218
+ );
4219
+ await store.saveState(state);
4220
+ return await requestDirectResponse(client, retryRuntimeConfig, state.messages);
4221
+ }
4222
+ }
4223
+
4158
4224
  await store.appendEvent("model.requested", {
4159
4225
  step: 1,
4160
4226
  provider: config.provider,
@@ -4168,7 +4234,10 @@ async function finishWithResponseOnlyModelTurn({ client, config, state, store, o
4168
4234
  mode: "response-only",
4169
4235
  });
4170
4236
 
4171
- const response = await requestDirectResponse(client, config, state.messages);
4237
+ const response = await requestWithResponseOnlyLocalContextRecovery({
4238
+ step: 1,
4239
+ mode: "response-only",
4240
+ });
4172
4241
  const rawAssistantMessage = response.choices[0]?.message;
4173
4242
  let result = responseOnlyResultFromMessage(rawAssistantMessage);
4174
4243
  let finalAssistantMessage = rawAssistantMessage;
@@ -4208,7 +4277,10 @@ async function finishWithResponseOnlyModelTurn({ client, config, state, store, o
4208
4277
  model: config.model,
4209
4278
  mode: "response-only-repair",
4210
4279
  });
4211
- const repairResponse = await requestDirectResponse(client, config, state.messages);
4280
+ const repairResponse = await requestWithResponseOnlyLocalContextRecovery({
4281
+ step: 2,
4282
+ mode: "response-only-repair",
4283
+ });
4212
4284
  finalAssistantMessage = repairResponse.choices[0]?.message;
4213
4285
  result = responseOnlyResultFromMessage(finalAssistantMessage);
4214
4286
  sourceFreeAssessment = await assessResponseOnlySourceFreeClaims({
@@ -30,7 +30,7 @@ const MAX_VALIDATION_ERRORS = 8;
30
30
  const MAX_VALIDATION_NODES = 50_000;
31
31
  const MAX_SAFE_SEQUENTIAL_READ_CALLS = 4;
32
32
  const MAX_REPORTED_SEQUENTIAL_CALLS = 12;
33
- const BENIGN_TOOL_CALL_ANNOTATION_KEYS = new Set(["description"]);
33
+ const BENIGN_TOOL_CALL_ANNOTATION_KEYS = new Set(["description", "reason"]);
34
34
 
35
35
  function cloneValue(value) {
36
36
  return structuredClone(value);