Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules/
dist/
scratchpad.md
**/.gitignore
**/.env
**/.env
debug.log
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ For more information, see the [documentation](docs/introduction.md).
Try the example chat interface:

```bash
npm install && npm run build
cd cli
npm install
echo "OPENAI_API_KEY=your_key_here" > .env
Expand Down
2 changes: 1 addition & 1 deletion cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"main": "index.js",
"type": "module",
"scripts": {
"dev": "vite-node -w ./src/index.tsx"
"dev": "vite-node ./src/index.tsx"
},
"author": "",
"license": "ISC",
Expand Down
1 change: 1 addition & 0 deletions cli/src/components/MessageBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const MessageBlock = React.memo(({ msg }: { msg: Message }) => {
}

if (msg.type === 'tool-results') {
console.log(msg);

Copilot AI Aug 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug console.log statement should be removed from production code. Consider using a proper logging library or removing this debugging statement.

Suggested change
console.log(msg);

Copilot uses AI. Check for mistakes.
return (
<Box marginTop={1} display="flex" flexDirection="column">
<Text backgroundColor="magenta" color="black">
Expand Down
14 changes: 14 additions & 0 deletions cli/src/tools/async-test-tool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createTool, createPromiseProcess } from '@unternet/kernel';
import { z } from 'zod';

export default createTool({
name: 'async_test_tool',
description: 'Test async tool-calling functionality.',
parameters: z.object({ name: z.string() }),
execute: () =>
createPromiseProcess('async_test_tool', () => {
return new Promise((resolve) => {
setTimeout(() => resolve('Test completed successfully!'), 4000);
});
}),
});
18 changes: 2 additions & 16 deletions cli/src/tools/deep-research.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createTool, PromiseProcess } from '@unternet/kernel';
import { createPromiseProcess, createTool } from '@unternet/kernel';
import { getJson } from 'serpapi';
import { z } from 'zod';

Expand Down Expand Up @@ -38,20 +38,6 @@ export default createTool({
description: 'Perform a research investigation for a more thorough answer.',
parameters: z.object({ query: z.string() }),
execute: ({ query }) => {
return new PromiseProcess('deep_research', () => deepResearch(query));
createPromiseProcess('deep_research', () => deepResearch(query));

Copilot AI Aug 8, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The createPromiseProcess function is called but its return value is not returned from the execute function. This will cause the tool to return undefined instead of the expected process constructor.

Suggested change
createPromiseProcess('deep_research', () => deepResearch(query));
return createPromiseProcess('deep_research', () => deepResearch(query));

Copilot uses AI. Check for mistakes.
},
});

// Consider having a `target:` property here
// We send a .call() to the target when it's instantiated
// That means the promise process/runtime can emit an event that contains
// all the call details, for the response message.
// We might actually want runtime.call(id, toolCall)?

// Or maybe we can just receive the call in execute, and
// run the call here? Then we can have everything responding to execute.

// Oh! Maybe if we return a Callable (e.g. Process), we then run call on that
// once we receive it and spawn it properly!

// Ah but then we can't encapsulate the real tool name in here...
3 changes: 2 additions & 1 deletion cli/src/tools/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Tool } from '@unternet/kernel';
import deepResearchTool from './deep-research';
import asyncTestTool from './async-test-tool';
import testTool from './test-tool';

export const tools = [testTool, deepResearchTool] as Tool[];
export const tools = [testTool, asyncTestTool, deepResearchTool] as Tool[];
2 changes: 1 addition & 1 deletion docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ If the kernel receives a `Process` object after executing the tool, it will spin
const meaningOfLifeTool = createTool({
name: 'meaning_of_life',
description: 'Get the answer to the meaning of life, the universe, and everything',
execute: () => return new MeaningOfLifeProcess(),
execute: () => return MeaningOfLifeProcess,
});

const tools = [meaningOfLifeTool];
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export * from './kernel';
export * from './types';
export * from './processes';
export * from './messages';
export * from './stream';
export * from './tools';
export * from './emitter';
export * from './processes';
export * from './promise-process';
86 changes: 57 additions & 29 deletions src/kernel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ import { Tool } from './tools';
import { createMessage, Message, ReplyMessage } from './messages';
import { ToolCall, ToolResult } from './tools';
import { Emitter } from './emitter';
import { Process } from './processes';
import { Process } from './processes/process';
import { ProcessContainer } from './processes/process-container';
import { DEFAULT_MESSAGE_LIMIT } from './constants';
import { ulid } from 'ulid';
import { isProcessConstructor } from './processes';

export interface KernelOpts {
model: LanguageModel;
Expand All @@ -25,11 +28,12 @@ type KernelStatus = 'idle' | 'busy';

export class Kernel extends Emitter<KernelEvents> {
model: LanguageModel;
tools: Tool[] = [];
messageLimit: number = DEFAULT_MESSAGE_LIMIT;
runtime = new Runtime();
tools: Tool[] = [];
spawn = this.runtime.spawn.bind(this.runtime);
restore = this.runtime.restore.bind(this.runtime);
kill = this.runtime.kill.bind(this.runtime);
registerProcessConstructor = this.runtime.registerProcessConstructor.bind(
this.runtime
);
Expand All @@ -44,21 +48,42 @@ export class Kernel extends Emitter<KernelEvents> {
if (opts.tools) this.tools = opts.tools;
if (opts.messages) this.messages = opts.messages;

this.runtime.on('process.created', (e) => {
this.emit('process.created', e);
this.runtime.on('process-created', (e) => {
this.emit('process-created', e);
});
this.runtime.on('process.changed', (e) => {
this.emit('process.changed', e);
this.runtime.on('process-changed', (e) => {
this.emit('process-changed', e);
});
this.runtime.on('process.exited', (e) => {
this.emit('process.exited', e);
this.runtime.on('process-exited', (e) => {
this.emit('process-exited', e);
});

this.runtime.on('process.tool-result', (e) => {
// TODO: Propert implementation of process results
this.runtime.on('tool-result', (e) => {
const callId = ulid();

// TODO: Make this actually reflect the tool call better
// Can we have multiple outputs to one tool call?
this.addMessage(
createMessage('tool-calls', {
calls: [
{
id: callId,
name: e.result.name || '',
args: {},
},
],
})
);

this.send(
createMessage('system', {
text: `Tool call completed.`,
createMessage('tool-results', {
results: [
{
callId,
name: e.result.name || '',
output: e.result.output,
},
],
})
);
});
Expand Down Expand Up @@ -124,18 +149,23 @@ export class Kernel extends Emitter<KernelEvents> {

// Handle deltas
if (response.type === 'delta') {
// Check if this is is the first chunk
if (!this._messages.has(response.id)) {
const initialMsg: ReplyMessage = {
type: 'reply',
id: response.id,
timestamp: response.timestamp,
text: '',
};
this.addMessage(initialMsg);
// Only process reply deltas at this stage
if (response.messageType === 'reply') {
const initialMsg: ReplyMessage = {
type: 'reply',
id: response.id,
timestamp: response.timestamp,
text: '',
};
this.addMessage(initialMsg);
}
}

let msg = this._messages.get(response.id);

// We have already received the first chunk, now append
if (msg && msg.type === 'reply') {
const delta = response.delta as Partial<ReplyMessage>;

Expand Down Expand Up @@ -173,22 +203,20 @@ export class Kernel extends Emitter<KernelEvents> {
const { id, name, args } = call;
const tool = this.tools.find((t) => t.name === name);

if (!tool?.execute) {
return;
// throw new Error(`Unknown or invalid tool: ${name}`);
}
if (!tool?.execute) return;

let rawOutput = await tool.execute(args);
let output = await tool.execute(args);

const output =
rawOutput instanceof Process
? this.runtime.spawn(rawOutput)
: rawOutput;
let container: ProcessContainer | null = null;
if (isProcessConstructor(output)) {
container = this.runtime.spawn(output);
container.call(call);
}

results.push({
callId: id,
name: name,
output,
output: container ?? output,
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
CoreUserMessage,
} from 'ai';
import { ToolCall, ToolResult } from './tools';
import { ProcessContainer } from './processes';
import { ProcessContainer } from './processes/process-container';

export type Message =
| SystemMessage
Expand Down
Loading