@aion0/forge 0.10.29 → 0.10.31

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/RELEASE_NOTES.md CHANGED
@@ -1,20 +1,11 @@
1
- # Forge v0.10.29
1
+ # Forge v0.10.31
2
2
 
3
3
  Released: 2026-06-02
4
4
 
5
- ## Changes since v0.10.28
5
+ ## Changes since v0.10.30
6
6
 
7
7
  ### Other
8
- - feat(ssh): template-expand spec.timeout_sec lets tools surface as arg
9
- - fix(help-content): drop import.meta.url branch — Turbopack rejects webpackIgnore
10
- - fix(help-content): suppress Turbopack 'cannot resolve ./help-docs' warning
11
- - fix(http): empty-string body resolves to no-body
12
- - fix(http-auth): bearer-token-exchange honours connector http.verify_tls
13
- - feat(connector-test): add body_match regex for response-body validation
14
- - fix(http): reorder method-template expand to AFTER argsWithDefaults
15
- - fix(http): expand templates in spec.method before uppercase
16
- - docs(connector-authoring): add bearer-token-exchange section to 21-build-connector
17
- - feat(http-auth): bearer-token-exchange supports body-mode + bare format
8
+ - fix(watch): allow builtin tool names (no connector prefix) in start_watch
18
9
 
19
10
 
20
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.28...v0.10.29
11
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.30...v0.10.31
@@ -41,7 +41,11 @@ import type {
41
41
  ToolUseBlock,
42
42
  } from './types';
43
43
 
44
- const MAX_ITERATIONS = 6;
44
+ // Set to 24: enough headroom for typical multi-step tool chains
45
+ // (search → paginate → drill → summarise can be 8-15; complex audits
46
+ // 15-20) without inviting runaway loops. Hitting it surfaces the
47
+ // sentinel message below so the user knows why the turn stopped.
48
+ const MAX_ITERATIONS = 24;
45
49
  const MAX_TOKENS = 16000;
46
50
  // Working-window budgets for the LLM history. Capped by message count
47
51
  // AND by token estimate (whichever hits first), see design §8. Older
@@ -596,6 +600,23 @@ export async function runTurn(args: RunTurnArgs): Promise<{ ok: boolean; error?:
596
600
  cb({ type: 'message_saved', message_id: toolResultMsg.id, data: toolResultMsg });
597
601
  }
598
602
 
603
+ // If we fell out of the while because we hit MAX_ITERATIONS (vs the
604
+ // model emitting a non-tool_use stopReason), the turn ends with no
605
+ // closing assistant text and the user sees a "no response" hang.
606
+ // Surface a sentinel assistant message so the cause is visible.
607
+ if (iter >= MAX_ITERATIONS && lastStop === 'tool_use') {
608
+ const sentinel = appendMessage({
609
+ session_id: args.sessionId,
610
+ role: 'assistant',
611
+ blocks: [{
612
+ type: 'text',
613
+ text: `⚠️ Hit the tool-call iteration limit (${MAX_ITERATIONS}) without finishing. Likely causes: a tool kept returning truncated/incomplete results so I kept retrying, or the task was too broad. Try narrowing the query or splitting it into smaller asks.`,
614
+ }],
615
+ });
616
+ cb({ type: 'message_saved', message_id: sentinel.id, data: sentinel });
617
+ cb({ type: 'error', data: { error: `iteration limit (${MAX_ITERATIONS}) exceeded` } });
618
+ }
619
+
599
620
  cb({ type: 'turn_done', data: { iterations: iter, stop_reason: lastStop } });
600
621
  return { ok: true };
601
622
  } catch (err) {
@@ -35,12 +35,12 @@ export function buildStartWatchTool(sessionId: string | null): StartWatchTool {
35
35
  const def: BuiltinToolDef = {
36
36
  name: 'start_watch',
37
37
  description:
38
- 'Register a BACKGROUND WATCH that polls a tool until done, then posts the result back here — for long-running jobs you just kicked off (a Jenkins build, a test run, a device upgrade). Use this INSTEAD of polling in conversation: call the trigger tool, then call start_watch and STOP — Forge polls in the background and a completion message arrives in this chat. ' +
39
- 'Pick `poll` = the read tool that reports status (e.g. "jenkins.get_build") and `poll_args` to call it with (e.g. the build number you predicted via get_next_build_number). Give a done condition: `done_match` {path, equals} on the poll result (e.g. path "result" equals "SUCCESS"), or `done_path` (a result path that becomes truthy). You usually already saw the poll tool\'s output once, so you know the right field. Optional `fail_path` (truthy = failed). Tune `interval_sec`/`timeout_sec` to the job (build ≈ 60s / 1800s).',
38
+ 'Register a BACKGROUND WATCH that polls a tool until done, then posts the result back here — for long-running jobs you just kicked off (a Forge pipeline, a Jenkins build, a test run, a device upgrade). Use this INSTEAD of polling in conversation: call the trigger tool, then call start_watch and STOP — Forge polls in the background and a completion message arrives in this chat. ' +
39
+ 'Pick `poll` = the read tool that reports status. Two forms accepted: (a) connector tool "<connector>.<tool>" e.g. "jenkins.get_build", "gitlab.get_pipeline"; (b) BARE builtin name e.g. "get_pipeline_status" (Forge pipelines — pair with poll_args={pipeline_id} and done_match={path:"status",equals:"done"}). Set `poll_args` to call it with (e.g. the build number you predicted via get_next_build_number). Give a done condition: `done_match` {path, equals} on the poll result (e.g. path "result" equals "SUCCESS"), or `done_path` (a result path that becomes truthy). You usually already saw the poll tool\'s output once, so you know the right field. Optional `fail_path` (truthy = failed). Tune `interval_sec`/`timeout_sec` to the job (pipeline ≈ 30s / 1800s, build ≈ 60s / 1800s).',
40
40
  input_schema: {
41
41
  type: 'object',
42
42
  properties: {
43
- poll: { type: 'string', description: 'Tool to poll, "<connector>.<tool>" e.g. "jenkins.get_build". Must be a read/status tool.' },
43
+ poll: { type: 'string', description: 'Tool to poll. Either "<connector>.<tool>" e.g. "jenkins.get_build", OR a bare builtin name e.g. "get_pipeline_status" (for Forge pipelines). Must be a read/status tool.' },
44
44
  poll_args: { type: 'object', description: 'Args to call the poll tool with each tick, e.g. {"job_path":"job/foo","build_number":18}. Concrete values, not templates.' },
45
45
  done_match: {
46
46
  type: 'object',
@@ -68,9 +68,16 @@ export function buildStartWatchTool(sessionId: string | null): StartWatchTool {
68
68
  const a = (input ?? {}) as Record<string, any>;
69
69
  const poll = String(a.poll || '').trim();
70
70
  const dot = poll.indexOf('.');
71
- if (dot < 1) return JSON.stringify({ ok: false, error: 'poll must be "<connector>.<tool>", e.g. jenkins.get_build' });
72
- const connectorId = poll.slice(0, dot);
73
- const pollTool = poll.slice(dot + 1);
71
+ // Two forms accepted:
72
+ // "connector.tool" → connector_id="connector", poll_tool="tool"
73
+ // "tool" → builtin (e.g. get_pipeline_status), connector_id=""
74
+ // The watch-runner detects empty connector_id and dispatches as a
75
+ // bare builtin name instead of "connector.tool".
76
+ const connectorId = dot >= 1 ? poll.slice(0, dot) : '';
77
+ const pollTool = dot >= 1 ? poll.slice(dot + 1) : poll;
78
+ if (!pollTool) {
79
+ return JSON.stringify({ ok: false, error: 'poll is required, e.g. "jenkins.get_build" or builtin "get_pipeline_status"' });
80
+ }
74
81
 
75
82
  const doneMatch = a.done_match && typeof a.done_match === 'object' && a.done_match.path
76
83
  ? { path: String(a.done_match.path), ...(a.done_match.equals != null ? { equals: String(a.done_match.equals) } : {}), ...(a.done_match.contains != null ? { contains: String(a.done_match.contains) } : {}) }
@@ -83,8 +90,8 @@ export function buildStartWatchTool(sessionId: string | null): StartWatchTool {
83
90
  return JSON.stringify({ ok: false, error: `active watch limit reached (${MAX_ACTIVE_WATCHES})` });
84
91
  }
85
92
 
86
- const hint = ['build_number', 'host', 'ip', 'lab', 'id', 'name'].map((k) => a.poll_args?.[k]).find((v) => v != null && v !== '');
87
- const label = `${connectorId}.${pollTool}${hint != null ? ` ${hint}` : ''}`;
93
+ const hint = ['build_number', 'host', 'ip', 'lab', 'id', 'name', 'pipeline_id'].map((k) => a.poll_args?.[k]).find((v) => v != null && v !== '');
94
+ const label = `${connectorId ? `${connectorId}.${pollTool}` : pollTool}${hint != null ? ` ${hint}` : ''}`;
88
95
 
89
96
  const w = createWatch({
90
97
  session_id: sessionId,
@@ -93,7 +93,10 @@ export function startWatchRunner(hooks: WatchRunnerHooks = {}): void {
93
93
  // preamble — the body comes back as raw JSON so done_path/done_match can
94
94
  // see it. (Without this, every http-protocol watch would silently never
95
95
  // hit its done condition, e.g. jenkins.get_build never resolving.)
96
- res = await dispatchTool({ id: `watch-${w.id}-${w.polls}`, name: `${w.connector_id}.${w.poll_tool}`, input: w.poll_args }, { noTruncation: true } as any);
96
+ // Empty connector_id builtin (e.g. get_pipeline_status); dispatch
97
+ // bare name so dispatchTool routes to the global BUILTINS table.
98
+ const dispatchName = w.connector_id ? `${w.connector_id}.${w.poll_tool}` : w.poll_tool;
99
+ res = await dispatchTool({ id: `watch-${w.id}-${w.polls}`, name: dispatchName, input: w.poll_args }, { noTruncation: true } as any);
97
100
  } catch (e) {
98
101
  res = { content: String(e), is_error: true };
99
102
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.10.29",
3
+ "version": "0.10.31",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {