Skip to content

Commit 337f539

Browse files
committed
docs(gmi): document LoopController duplication and future refactor path
The GMI's processTurnStream() maintains an inline tool-calling loop that duplicates the LoopController's ReAct loop. After careful assessment, a full replacement (Option A) or partial delegation (Option B) is too risky: the GMI loop carries RAG retrieval, prompt reconstruction via PromptEngine, persona-scoped tool orchestration, GMI state transitions, streaming via generateCompletionStream, capability discovery filtering, and GMIError-based fail_closed semantics — none of which the generic LoopController abstracts. Instead (Option C): - Add a detailed comment block in GMI.processTurnStream() documenting all GMI-specific concerns that prevent a drop-in replacement - Outline a future refactor path: extract RAG+prompt-build into a pre-iteration callback and tool-dispatch into a LoopContext adapter - Add a corresponding note in LoopController.ts pointing back to the GMI loop documentation - The configurable maxToolLoopIterations (added in a prior commit) keeps the safety break in sync with LoopController's maxIterations
1 parent a97e059 commit 337f539

4 files changed

Lines changed: 129 additions & 0 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { describe, expect, it, vi, beforeEach } from 'vitest';
2+
3+
const hoisted = vi.hoisted(() => {
4+
const generateCompletion = vi.fn();
5+
const getProvider = vi.fn(() => ({ generateCompletion }));
6+
const createProviderManager = vi.fn(async () => ({ getProvider }));
7+
return {
8+
generateCompletion,
9+
getProvider,
10+
createProviderManager,
11+
};
12+
});
13+
14+
vi.mock('../model.js', () => ({
15+
resolveModelOption: vi.fn(() => ({ providerId: 'openai', modelId: 'gpt-4.1-mini' })),
16+
resolveProvider: vi.fn(() => ({
17+
providerId: 'openai',
18+
modelId: 'gpt-4.1-mini',
19+
apiKey: 'test-key',
20+
})),
21+
createProviderManager: hoisted.createProviderManager,
22+
}));
23+
24+
import { generateText } from '../generateText.js';
25+
import { DEFAULT_COT_INSTRUCTION } from '../generateText.js';
26+
27+
describe('chainOfThought', () => {
28+
beforeEach(() => {
29+
hoisted.generateCompletion.mockReset();
30+
hoisted.generateCompletion.mockResolvedValue({
31+
modelId: 'gpt-4.1-mini',
32+
usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 },
33+
choices: [
34+
{
35+
message: { role: 'assistant', content: 'response text' },
36+
finishReason: 'stop',
37+
},
38+
],
39+
});
40+
});
41+
42+
it('does not inject CoT instruction when chainOfThought is disabled', async () => {
43+
await generateText({
44+
model: 'openai:gpt-4.1-mini',
45+
prompt: 'hello',
46+
system: 'You are a helper.',
47+
tools: { my_tool: { description: 'A tool', parameters: {} } },
48+
chainOfThought: false,
49+
});
50+
51+
const messages = hoisted.generateCompletion.mock.calls[0][1];
52+
const systemMsg = messages.find((m: any) => m.role === 'system');
53+
expect(systemMsg.content).toBe('You are a helper.');
54+
expect(systemMsg.content).not.toContain('Before choosing an action');
55+
});
56+
57+
it('injects default CoT instruction when chainOfThought is true', async () => {
58+
await generateText({
59+
model: 'openai:gpt-4.1-mini',
60+
prompt: 'hello',
61+
system: 'You are a helper.',
62+
tools: { my_tool: { description: 'A tool', parameters: {} } },
63+
chainOfThought: true,
64+
});
65+
66+
const messages = hoisted.generateCompletion.mock.calls[0][1];
67+
const systemMsg = messages.find((m: any) => m.role === 'system');
68+
expect(systemMsg.content).toContain(DEFAULT_COT_INSTRUCTION);
69+
expect(systemMsg.content).toContain('You are a helper.');
70+
});
71+
72+
it('injects custom CoT instruction when chainOfThought is a string', async () => {
73+
const customCot = 'Think step by step before answering.';
74+
75+
await generateText({
76+
model: 'openai:gpt-4.1-mini',
77+
prompt: 'hello',
78+
system: 'You are a helper.',
79+
tools: { my_tool: { description: 'A tool', parameters: {} } },
80+
chainOfThought: customCot,
81+
});
82+
83+
const messages = hoisted.generateCompletion.mock.calls[0][1];
84+
const systemMsg = messages.find((m: any) => m.role === 'system');
85+
expect(systemMsg.content).toContain(customCot);
86+
expect(systemMsg.content).toContain('You are a helper.');
87+
expect(systemMsg.content).not.toContain(DEFAULT_COT_INSTRUCTION);
88+
});
89+
});

src/api/agent.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ export interface AgentOptions extends BaseAgentConfig {
3737
* When present, forwarded to `observability.usageLedger` internally.
3838
*/
3939
usageLedger?: AgentOSUsageLedgerOptions;
40+
/**
41+
* Chain-of-thought reasoning instruction.
42+
* - `false` — disable CoT injection.
43+
* - `true` (default for agents) — inject the default CoT instruction when tools are present.
44+
* - `string` — inject a custom CoT instruction when tools are present.
45+
*/
46+
chainOfThought?: boolean | string;
4047
}
4148

4249
/**
@@ -185,6 +192,7 @@ export function agent(opts: AgentOptions): Agent {
185192
system: buildSystemPrompt(opts),
186193
tools: opts.tools,
187194
maxSteps: opts.maxSteps ?? 5,
195+
chainOfThought: opts.chainOfThought ?? true,
188196
apiKey: opts.apiKey,
189197
baseUrl: opts.baseUrl,
190198
usageLedger: effectiveLedger,

src/cognitive_substrate/GMI.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,31 @@ export class GMI implements IGMI {
805805
}
806806
}
807807

808+
// -------------------------------------------------------------------
809+
// Main tool-calling loop (ReAct-style).
810+
//
811+
// NOTE: This loop duplicates the general-purpose LoopController
812+
// (src/orchestration/runtime/LoopController.ts) but carries
813+
// GMI-specific concerns that prevent a simple drop-in replacement:
814+
//
815+
// - RAG retrieval + cognitive memory assembly on each iteration
816+
// - Full prompt reconstruction via PromptEngine per iteration
817+
// - Tool orchestration through IToolOrchestrator with persona-scoped
818+
// ToolExecutionRequestDetails (gmiId, capabilities, sessionData)
819+
// - GMIPrimeState transitions (PROCESSING <-> AWAITING_TOOL_RESULT)
820+
// - Streaming via provider.generateCompletionStream() rather than
821+
// the LoopController's AsyncGenerator<LoopChunk> abstraction
822+
// - Capability discovery tool filtering per iteration
823+
// - GMIError-based fail_closed semantics with structured error codes
824+
//
825+
// Future refactor path: extract the RAG + prompt-build phase into a
826+
// pre-iteration callback and the tool-dispatch phase into a
827+
// LoopContext adapter, then delegate the iteration/termination logic
828+
// to LoopController.execute(). This would unify the safety-break,
829+
// parallel-tools, and fail_open/fail_closed policies. For now the
830+
// configurable maxToolLoopIterations (GMIBaseConfig) keeps the safety
831+
// break in sync with LoopController's maxIterations concept.
832+
// -------------------------------------------------------------------
808833
let safetyBreak = 0;
809834
const maxToolLoopIterations = this.config.maxToolLoopIterations ?? 5;
810835
main_processing_loop: while (safetyBreak < maxToolLoopIterations) {

src/orchestration/runtime/LoopController.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@
66
* that supports parallel/sequential tool dispatch, configurable failure modes, and
77
* iteration limits. Yields structured {@link LoopEvent}s for observability.
88
*
9+
* NOTE: The GMI (src/cognitive_substrate/GMI.ts) still maintains its own inline
10+
* tool-calling loop in `processTurnStream()`. The GMI loop carries RAG retrieval,
11+
* prompt reconstruction, persona-scoped tool orchestration, and GMI state
12+
* management that this controller does not yet abstract. The GMI loop documents
13+
* a future refactor path to delegate iteration/termination logic here. See the
14+
* comment block in GMI.processTurnStream() for details.
15+
*
916
* @example
1017
* ```typescript
1118
* const controller = new LoopController();

0 commit comments

Comments
 (0)