@link-assistant/agent 0.5.3 → 0.6.1

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.
@@ -36,16 +36,20 @@ export namespace State {
36
36
  const entries = recordsByKey.get(key);
37
37
  if (!entries) return;
38
38
 
39
- log.info('waiting for state disposal to complete', { key });
39
+ log.info(() => ({
40
+ message: 'waiting for state disposal to complete',
41
+ key,
42
+ }));
40
43
 
41
44
  let disposalFinished = false;
42
45
 
43
46
  setTimeout(() => {
44
47
  if (!disposalFinished) {
45
- log.warn(
46
- 'state disposal is taking an unusually long time - if it does not complete in a reasonable time, please report this as a bug',
47
- { key }
48
- );
48
+ log.warn(() => ({
49
+ message:
50
+ 'state disposal is taking an unusually long time - if it does not complete in a reasonable time, please report this as a bug',
51
+ key,
52
+ }));
49
53
  }
50
54
  }, 10000).unref();
51
55
 
@@ -56,7 +60,11 @@ export namespace State {
56
60
  const task = Promise.resolve(entry.state)
57
61
  .then((state) => entry.dispose!(state))
58
62
  .catch((error) => {
59
- log.error('Error while disposing state:', { error, key });
63
+ log.error(() => ({
64
+ message: 'Error while disposing state',
65
+ error,
66
+ key,
67
+ }));
60
68
  });
61
69
 
62
70
  tasks.push(task);
@@ -64,6 +72,6 @@ export namespace State {
64
72
  await Promise.all(tasks);
65
73
  recordsByKey.delete(key);
66
74
  disposalFinished = true;
67
- log.info('state disposal completed', { key });
75
+ log.info(() => ({ message: 'state disposal completed', key }));
68
76
  }
69
77
  }
@@ -78,18 +78,17 @@ export namespace ModelsDev {
78
78
 
79
79
  export async function refresh() {
80
80
  const file = Bun.file(filepath);
81
- log.info('refreshing', {
82
- file,
83
- });
81
+ log.info(() => ({ message: 'refreshing', file }));
84
82
  const result = await fetch('https://models.dev/api.json', {
85
83
  headers: {
86
84
  'User-Agent': 'agent-cli/1.0.0',
87
85
  },
88
86
  signal: AbortSignal.timeout(10 * 1000),
89
87
  }).catch((e) => {
90
- log.error('Failed to fetch models.dev', {
88
+ log.error(() => ({
89
+ message: 'Failed to fetch models.dev',
91
90
  error: e,
92
- });
91
+ }));
93
92
  });
94
93
  if (result && result.ok) await Bun.write(file, await result.text());
95
94
  }
@@ -37,7 +37,7 @@ export namespace Provider {
37
37
  // Check if OAuth credentials are available via the auth plugin
38
38
  const auth = await Auth.get('anthropic');
39
39
  if (auth?.type === 'oauth') {
40
- log.info('using anthropic oauth credentials');
40
+ log.info(() => ({ message: 'using anthropic oauth credentials' }));
41
41
  const loaderFn = await AuthPlugins.getLoader('anthropic');
42
42
  if (loaderFn) {
43
43
  const result = await loaderFn(() => Auth.get('anthropic'), input);
@@ -330,7 +330,7 @@ export namespace Provider {
330
330
  google: async (input) => {
331
331
  const auth = await Auth.get('google');
332
332
  if (auth?.type === 'oauth') {
333
- log.info('using google oauth credentials');
333
+ log.info(() => ({ message: 'using google oauth credentials' }));
334
334
  const loaderFn = await AuthPlugins.getLoader('google');
335
335
  if (loaderFn) {
336
336
  const result = await loaderFn(() => Auth.get('google'), input);
@@ -355,7 +355,9 @@ export namespace Provider {
355
355
  'github-copilot': async (input) => {
356
356
  const auth = await Auth.get('github-copilot');
357
357
  if (auth?.type === 'oauth') {
358
- log.info('using github copilot oauth credentials');
358
+ log.info(() => ({
359
+ message: 'using github copilot oauth credentials',
360
+ }));
359
361
  const loaderFn = await AuthPlugins.getLoader('github-copilot');
360
362
  if (loaderFn) {
361
363
  const result = await loaderFn(
@@ -383,7 +385,9 @@ export namespace Provider {
383
385
  'github-copilot-enterprise': async (input) => {
384
386
  const auth = await Auth.get('github-copilot-enterprise');
385
387
  if (auth?.type === 'oauth') {
386
- log.info('using github copilot enterprise oauth credentials');
388
+ log.info(() => ({
389
+ message: 'using github copilot enterprise oauth credentials',
390
+ }));
387
391
  const loaderFn = await AuthPlugins.getLoader('github-copilot');
388
392
  if (loaderFn) {
389
393
  const result = await loaderFn(
@@ -435,7 +439,10 @@ export namespace Provider {
435
439
  return { autoload: false };
436
440
  }
437
441
 
438
- log.info('using claude oauth credentials', { source: tokenSource });
442
+ log.info(() => ({
443
+ message: 'using claude oauth credentials',
444
+ source: tokenSource,
445
+ }));
439
446
 
440
447
  // Create authenticated fetch with Bearer token and OAuth beta header
441
448
  const customFetch = ClaudeOAuth.createAuthenticatedFetch(oauthToken);
@@ -537,7 +544,7 @@ export namespace Provider {
537
544
  // Maps `${provider}/${key}` to the provider’s actual model ID for custom aliases.
538
545
  const realIdByKey = new Map<string, string>();
539
546
 
540
- log.info('init');
547
+ log.info(() => ({ message: 'init' }));
541
548
 
542
549
  function mergeProvider(
543
550
  id: string,
@@ -783,7 +790,7 @@ export namespace Provider {
783
790
  delete providers[providerID];
784
791
  continue;
785
792
  }
786
- log.info('found', { providerID });
793
+ log.info(() => ({ message: 'found', providerID }));
787
794
  }
788
795
 
789
796
  return {
@@ -818,19 +825,21 @@ export namespace Provider {
818
825
 
819
826
  let installedPath: string;
820
827
  if (!pkg.startsWith('file://')) {
821
- log.info('installing provider package', {
828
+ log.info(() => ({
829
+ message: 'installing provider package',
822
830
  providerID: provider.id,
823
831
  pkg,
824
832
  version: 'latest',
825
- });
833
+ }));
826
834
  installedPath = await BunProc.install(pkg, 'latest');
827
- log.info('provider package installed successfully', {
835
+ log.info(() => ({
836
+ message: 'provider package installed successfully',
828
837
  providerID: provider.id,
829
838
  pkg,
830
839
  installedPath,
831
- });
840
+ }));
832
841
  } else {
833
- log.info('loading local provider', { pkg });
842
+ log.info(() => ({ message: 'loading local provider', pkg }));
834
843
  installedPath = pkg;
835
844
  }
836
845
 
@@ -876,13 +885,14 @@ export namespace Provider {
876
885
  s.sdk.set(key, loaded);
877
886
  return loaded as SDK;
878
887
  })().catch((e) => {
879
- log.error('provider initialization failed', {
888
+ log.error(() => ({
889
+ message: 'provider initialization failed',
880
890
  providerID: provider.id,
881
891
  pkg: model.provider?.npm ?? provider.npm ?? provider.id,
882
892
  error: e instanceof Error ? e.message : String(e),
883
893
  stack: e instanceof Error ? e.stack : undefined,
884
894
  cause: e instanceof Error && e.cause ? String(e.cause) : undefined,
885
- });
895
+ }));
886
896
  throw new InitError({ providerID: provider.id }, { cause: e });
887
897
  });
888
898
  }
@@ -896,10 +906,7 @@ export namespace Provider {
896
906
  const s = await state();
897
907
  if (s.models.has(key)) return s.models.get(key)!;
898
908
 
899
- log.info('getModel', {
900
- providerID,
901
- modelID,
902
- });
909
+ log.info(() => ({ message: 'getModel', providerID, modelID }));
903
910
 
904
911
  const provider = s.providers[providerID];
905
912
  if (!provider) throw new ModelNotFoundError({ providerID, modelID });
@@ -929,7 +936,7 @@ export namespace Provider {
929
936
  ? await provider.getModel(sdk, realID, provider.options)
930
937
  : sdk.languageModel(realID);
931
938
  }
932
- log.info('found', { providerID, modelID });
939
+ log.info(() => ({ message: 'found', providerID, modelID }));
933
940
  s.models.set(key, {
934
941
  providerID,
935
942
  modelID,
@@ -1032,10 +1039,11 @@ export namespace Provider {
1032
1039
  if (opencodeProvider) {
1033
1040
  const [model] = sort(Object.values(opencodeProvider.info.models));
1034
1041
  if (model) {
1035
- log.info('using opencode provider as default', {
1042
+ log.info(() => ({
1043
+ message: 'using opencode provider as default',
1036
1044
  provider: opencodeProvider.info.id,
1037
1045
  model: model.id,
1038
- });
1046
+ }));
1039
1047
  return {
1040
1048
  providerID: opencodeProvider.info.id,
1041
1049
  modelID: model.id,
@@ -55,9 +55,7 @@ export namespace Server {
55
55
  export const App = lazy(() =>
56
56
  app
57
57
  .onError((err, c) => {
58
- log.error('failed', {
59
- error: err,
60
- });
58
+ log.error(() => ({ message: 'failed', error: err }));
61
59
  if (err instanceof NamedError) {
62
60
  let status: ContentfulStatusCode;
63
61
  if (err instanceof Storage.NotFoundError) status = 404;
@@ -71,10 +69,11 @@ export namespace Server {
71
69
  });
72
70
  })
73
71
  .use(async (c, next) => {
74
- log.info('request', {
72
+ log.info(() => ({
73
+ message: 'request',
75
74
  method: c.req.method,
76
75
  path: c.req.path,
77
- });
76
+ }));
78
77
  const timer = log.time('request', {
79
78
  method: c.req.method,
80
79
  path: c.req.path,
@@ -96,8 +96,22 @@ export class Agent {
96
96
  const errorTime = Date.now();
97
97
  const callID = `call_${Math.floor(Math.random() * 100000000)}`;
98
98
 
99
- // Log full error to stderr for debugging
100
- console.error('Tool execution error:', error);
99
+ // Log full error to stderr for debugging in JSON format
100
+ console.error(
101
+ JSON.stringify({
102
+ log: {
103
+ level: 'error',
104
+ timestamp: new Date().toISOString(),
105
+ message: 'Tool execution error',
106
+ tool: tool.name,
107
+ error: {
108
+ name: error.name,
109
+ message: error.message,
110
+ stack: error.stack,
111
+ },
112
+ },
113
+ })
114
+ );
101
115
 
102
116
  // Emit tool_use event with error
103
117
  this.emitEvent('tool_use', {
@@ -52,7 +52,7 @@ export namespace SessionCompaction {
52
52
  // tool calls that are no longer relevant.
53
53
  export async function prune(input: { sessionID: string }) {
54
54
  if (Flag.OPENCODE_DISABLE_PRUNE) return;
55
- log.info('pruning');
55
+ log.info(() => ({ message: 'pruning' }));
56
56
  const msgs = await Session.messages({ sessionID: input.sessionID });
57
57
  let total = 0;
58
58
  let pruned = 0;
@@ -78,7 +78,7 @@ export namespace SessionCompaction {
78
78
  }
79
79
  }
80
80
  }
81
- log.info('found', { pruned, total });
81
+ log.info(() => ({ message: 'found', pruned, total }));
82
82
  if (pruned > PRUNE_MINIMUM) {
83
83
  for (const part of toPrune) {
84
84
  if (part.state.status === 'completed') {
@@ -86,7 +86,7 @@ export namespace SessionCompaction {
86
86
  await Session.updatePart(part);
87
87
  }
88
88
  }
89
- log.info('pruned', { count: toPrune.length });
89
+ log.info(() => ({ message: 'pruned', count: toPrune.length }));
90
90
  }
91
91
  }
92
92
 
@@ -139,9 +139,7 @@ export namespace SessionCompaction {
139
139
  const result = await processor.process(() =>
140
140
  streamText({
141
141
  onError(error) {
142
- log.error('stream error', {
143
- error,
144
- });
142
+ log.error(() => ({ message: 'stream error', error }));
145
143
  },
146
144
  // set to 0, we handle loop
147
145
  maxRetries: 0,
@@ -178,7 +178,7 @@ export namespace Session {
178
178
  updated: Date.now(),
179
179
  },
180
180
  };
181
- log.info('created', result);
181
+ log.info(() => ({ message: 'created', ...result }));
182
182
  await Storage.write(['session', Instance.project.id, result.id], result);
183
183
  Bus.publish(Event.Created, {
184
184
  info: result,
@@ -273,7 +273,7 @@ export namespace Session {
273
273
  info: session,
274
274
  });
275
275
  } catch (e) {
276
- log.error(e);
276
+ log.error(() => ({ error: e }));
277
277
  }
278
278
  });
279
279
 
@@ -39,7 +39,7 @@ export namespace SessionProcessor {
39
39
  return toolcalls[toolCallID];
40
40
  },
41
41
  async process(fn: () => StreamTextResult<Record<string, AITool>, never>) {
42
- log.info('process');
42
+ log.info(() => ({ message: 'process' }));
43
43
  while (true) {
44
44
  try {
45
45
  let currentText: MessageV2.TextPart | undefined;
@@ -305,16 +305,12 @@ export namespace SessionProcessor {
305
305
  break;
306
306
 
307
307
  default:
308
- log.info('unhandled', {
309
- ...value,
310
- });
308
+ log.info(() => ({ message: 'unhandled', ...value }));
311
309
  continue;
312
310
  }
313
311
  }
314
312
  } catch (e) {
315
- log.error('process', {
316
- error: e,
317
- });
313
+ log.error(() => ({ message: 'process', error: e }));
318
314
  const error = MessageV2.fromError(e, {
319
315
  providerID: input.providerID,
320
316
  });
@@ -216,7 +216,7 @@ export namespace SessionPrompt {
216
216
  }
217
217
 
218
218
  export function cancel(sessionID: string) {
219
- log.info('cancel', { sessionID });
219
+ log.info(() => ({ message: 'cancel', sessionID }));
220
220
  const s = state();
221
221
  const match = s[sessionID];
222
222
  if (!match) return;
@@ -242,7 +242,7 @@ export namespace SessionPrompt {
242
242
 
243
243
  let step = 0;
244
244
  while (true) {
245
- log.info('loop', { step, sessionID });
245
+ log.info(() => ({ message: 'loop', step, sessionID }));
246
246
  if (abort.aborted) break;
247
247
  let msgs = await MessageV2.filterCompacted(MessageV2.stream(sessionID));
248
248
 
@@ -276,7 +276,7 @@ export namespace SessionPrompt {
276
276
  lastAssistant.finish !== 'tool-calls' &&
277
277
  lastUser.id < lastAssistant.id
278
278
  ) {
279
- log.info('exiting loop', { sessionID });
279
+ log.info(() => ({ message: 'exiting loop', sessionID }));
280
280
  break;
281
281
  }
282
282
 
@@ -297,14 +297,13 @@ export namespace SessionPrompt {
297
297
  lastUser.model.modelID
298
298
  );
299
299
  } catch (error) {
300
- log.warn(
301
- 'Failed to initialize specified model, falling back to default model',
302
- {
303
- providerID: lastUser.model.providerID,
304
- modelID: lastUser.model.modelID,
305
- error: error instanceof Error ? error.message : String(error),
306
- }
307
- );
300
+ log.warn(() => ({
301
+ message:
302
+ 'Failed to initialize specified model, falling back to default model',
303
+ providerID: lastUser.model.providerID,
304
+ modelID: lastUser.model.modelID,
305
+ error: error instanceof Error ? error.message : String(error),
306
+ }));
308
307
  const defaultModel = await Provider.defaultModel();
309
308
  model = await Provider.getModel(
310
309
  defaultModel.providerID,
@@ -551,53 +550,79 @@ export namespace SessionPrompt {
551
550
  );
552
551
  const totalEstimatedTokens = systemTokens + userTokens;
553
552
 
554
- log.info('=== VERBOSE: API Request Details ===');
555
- log.info(`Model: ${model.providerID}/${model.modelID}`);
556
- log.info(`Session ID: ${sessionID}`);
557
- log.info(`Agent: ${agent.name}`);
558
- log.info(`Temperature: ${params.temperature ?? 'default'}`);
559
- log.info(`Top P: ${params.topP ?? 'default'}`);
560
- log.info(
561
- `Active Tools: ${Object.keys(tools)
562
- .filter((x) => x !== 'invalid')
563
- .join(', ')}`
564
- );
565
- log.info('--- System Prompt ---');
553
+ log.info(() => ({
554
+ message: '=== VERBOSE: API Request Details ===',
555
+ }));
556
+ log.info(() => ({
557
+ message: 'Model',
558
+ model: `${model.providerID}/${model.modelID}`,
559
+ }));
560
+ log.info(() => ({ message: 'Session ID', sessionID }));
561
+ log.info(() => ({ message: 'Agent', agent: agent.name }));
562
+ log.info(() => ({
563
+ message: 'Temperature',
564
+ temperature: params.temperature ?? 'default',
565
+ }));
566
+ log.info(() => ({
567
+ message: 'Top P',
568
+ topP: params.topP ?? 'default',
569
+ }));
570
+ log.info(() => ({
571
+ message: 'Active Tools',
572
+ tools: Object.keys(tools).filter((x) => x !== 'invalid'),
573
+ }));
574
+ log.info(() => ({ message: '--- System Prompt ---' }));
566
575
  for (let i = 0; i < system.length; i++) {
567
576
  const tokens = Token.estimate(system[i]);
568
- log.info(`System Message ${i + 1} (${tokens} tokens estimated):`);
569
- log.info(
570
- system[i].slice(0, 2000) +
571
- (system[i].length > 2000 ? '... [truncated]' : '')
572
- );
577
+ log.info(() => ({
578
+ message: 'System Message',
579
+ index: i + 1,
580
+ tokens,
581
+ }));
582
+ log.info(() => ({
583
+ message: 'System Message Content',
584
+ content:
585
+ system[i].slice(0, 2000) +
586
+ (system[i].length > 2000 ? '... [truncated]' : ''),
587
+ }));
573
588
  }
574
- log.info('--- Token Summary ---');
575
- log.info(`System prompt tokens (estimated): ${systemTokens}`);
576
- log.info(`User message tokens (estimated): ${userTokens}`);
577
- log.info(`Total estimated tokens: ${totalEstimatedTokens}`);
578
- log.info(
579
- `Model context limit: ${model.info?.limit?.context || 'unknown'}`
580
- );
581
- log.info(
582
- `Model output limit: ${model.info?.limit?.output || 'unknown'}`
583
- );
584
- log.info('=== END VERBOSE ===');
589
+ log.info(() => ({ message: '--- Token Summary ---' }));
590
+ log.info(() => ({
591
+ message: 'System prompt tokens (estimated)',
592
+ systemTokens,
593
+ }));
594
+ log.info(() => ({
595
+ message: 'User message tokens (estimated)',
596
+ userTokens,
597
+ }));
598
+ log.info(() => ({
599
+ message: 'Total estimated tokens',
600
+ totalEstimatedTokens,
601
+ }));
602
+ log.info(() => ({
603
+ message: 'Model context limit',
604
+ contextLimit: model.info?.limit?.context || 'unknown',
605
+ }));
606
+ log.info(() => ({
607
+ message: 'Model output limit',
608
+ outputLimit: model.info?.limit?.output || 'unknown',
609
+ }));
610
+ log.info(() => ({ message: '=== END VERBOSE ===' }));
585
611
  }
586
612
 
587
613
  const result = await processor.process(() =>
588
614
  streamText({
589
615
  onError(error) {
590
- log.error('stream error', {
591
- error,
592
- });
616
+ log.error(() => ({ message: 'stream error', error }));
593
617
  },
594
618
  async experimental_repairToolCall(input) {
595
619
  const lower = input.toolCall.toolName.toLowerCase();
596
620
  if (lower !== input.toolCall.toolName && tools[lower]) {
597
- log.info('repairing tool call', {
621
+ log.info(() => ({
622
+ message: 'repairing tool call',
598
623
  tool: input.toolCall.toolName,
599
624
  repaired: lower,
600
- });
625
+ }));
601
626
  return {
602
627
  ...input.toolCall,
603
628
  toolName: lower,
@@ -945,7 +970,7 @@ export namespace SessionPrompt {
945
970
  }
946
971
  break;
947
972
  case 'file:':
948
- log.info('file', { mime: part.mime });
973
+ log.info(() => ({ message: 'file', mime: part.mime }));
949
974
  // have to normalize, symbol search returns absolute paths
950
975
  // Decode the pathname since URL constructor doesn't automatically decode it
951
976
  const filepath = fileURLToPath(part.url);
@@ -1012,7 +1037,10 @@ export namespace SessionPrompt {
1012
1037
  );
1013
1038
  })
1014
1039
  .catch((error) => {
1015
- log.error('failed to read file', { error });
1040
+ log.error(() => ({
1041
+ message: 'failed to read file',
1042
+ error,
1043
+ }));
1016
1044
  const message =
1017
1045
  error instanceof Error ? error.message : error.toString();
1018
1046
  Bus.publish(Session.Event.Error, {
@@ -1376,7 +1404,7 @@ export namespace SessionPrompt {
1376
1404
  */
1377
1405
 
1378
1406
  export async function command(input: CommandInput) {
1379
- log.info('command', input);
1407
+ log.info(() => ({ message: 'command', ...input }));
1380
1408
  const command = await Command.get(input.command);
1381
1409
  const agentName = command.agent ?? input.agent ?? 'build';
1382
1410
 
@@ -1572,10 +1600,11 @@ export namespace SessionPrompt {
1572
1600
  });
1573
1601
  })
1574
1602
  .catch((error) => {
1575
- log.error('failed to generate title', {
1603
+ log.error(() => ({
1604
+ message: 'failed to generate title',
1576
1605
  error,
1577
1606
  model: small.info?.id ?? small.modelID,
1578
- });
1607
+ }));
1579
1608
  });
1580
1609
  }
1581
1610
  }
@@ -72,7 +72,7 @@ export namespace SessionRevert {
72
72
  }
73
73
 
74
74
  export async function unrevert(input: { sessionID: string }) {
75
- log.info('unreverting', input);
75
+ log.info(() => ({ message: 'unreverting', ...input }));
76
76
  SessionPrompt.assertNotBusy(input.sessionID);
77
77
  const session = await Session.get(input.sessionID);
78
78
  if (!session.revert) return session;
@@ -115,7 +115,7 @@ export namespace SessionSummary {
115
115
  headers: small.info.headers,
116
116
  model: small.language,
117
117
  });
118
- log.info('title', { title: result.text });
118
+ log.info(() => ({ message: 'title', title: result.text }));
119
119
  userMsg.summary.title = result.text;
120
120
  await Session.updateMessage(userMsg);
121
121
  }
@@ -152,7 +152,7 @@ export namespace SessionSummary {
152
152
  if (result) summary = result.text;
153
153
  }
154
154
  userMsg.summary.body = summary;
155
- log.info('body', { body: summary });
155
+ log.info(() => ({ message: 'body', body: summary }));
156
156
  await Session.updateMessage(userMsg);
157
157
  }
158
158
  }