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
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ WORKDIR $homedir
RUN mkdir ".husky"
COPY .husky/install.mjs .husky/install.mjs

COPY package*.json ./
COPY package*.json pnpm-lock.yaml ./
RUN pnpm install

COPY . .
Expand Down
5 changes: 3 additions & 2 deletions build.js
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ async function runNpmScriptOrCommandInDirs(scriptOrCommandName, directories) {
currentNpmSubCommandForDir += ':ci';
}

const commandToExecuteInDir = `npm ${currentNpmSubCommandForDir}`;
const pkgMgr = dirInfo.path.includes('frontend') ? 'npm' : 'pnpm';
const commandToExecuteInDir = `${pkgMgr} ${currentNpmSubCommandForDir}`;

// Use the base script/command name for the task identifier for consistency
const taskName = `${dirInfo.name}-${scriptOrCommandName}`; // e.g., "frontend-build", "root-install"
Expand Down Expand Up @@ -144,7 +145,7 @@ async function runNpmScriptOrCommandInDirs(scriptOrCommandName, directories) {
}
});

const overallCommandDescription = `npm ${baseNpmSubCommand}`;
const overallCommandDescription = `${pkgMgr} ${baseNpmSubCommand}`;
if (anyFailed) {
console.error(`\n>>> "${overallCommandDescription}" failed in one or more directories. <<<`);
return false; // Indicate failure
Expand Down
38 changes: 30 additions & 8 deletions frontend/src/app/modules/prompts/form/prompt-form.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -290,20 +290,29 @@ <h4 class="text-xl font-semibold pb-4">
<div class="flex items-center justify-between mb-4">
<h2 class="text-xl font-semibold">Response</h2>
<div class="flex items-center gap-2">
<button *ngIf="generationResponse()"
<button *ngIf="generationResult()"
mat-stroked-button
color="primary"
(click)="addResponseToPrompt()"
matTooltip="Add response to prompt messages">
<mat-icon class="mr-2">add</mat-icon>
Add to prompt
</button>
<button *ngIf="generationResponse()" mat-icon-button matTooltip="Copy Response">
<button *ngIf="generationResult()" mat-icon-button matTooltip="Copy Response">
<mat-icon>content_copy</mat-icon>
</button>
</div>
</div>

<!-- Add this block -->
<div *ngIf="generationResult() as result" class="mb-3 text-sm">
<strong>Request Time:</strong> {{ result.createdAt | date : 'medium' }}. &nbsp;&nbsp;
<strong>Total Time:</strong> {{ ((result.totalTime ?? 0) / 1000).toFixed(1) }}s&nbsp;&nbsp;
<strong>Tokens in/out:</strong> {{ result.inputTokens }}/{{ result.outputTokens }}&nbsp;&nbsp;
<strong>Cost: </strong> ${{ result.cost?.toFixed(4) }}&nbsp;&nbsp;
<strong>Tok/S:</strong> {{ (result.outputTokens > 0 && result.totalTime > 0 ? (result.outputTokens / (result.totalTime / 1000)) : 0).toFixed(1) }}
</div>

<!-- Loading State -->
<div *ngIf="isGenerating()" class="flex flex-col items-center justify-center p-8">
<mat-spinner diameter="40"></mat-spinner>
Expand All @@ -316,14 +325,27 @@ <h2 class="text-xl font-semibold">Response</h2>
</div>

<!-- Response Content -->
<div *ngIf="generationResponse() && !isGenerating()" class="prose max-w-none">
<div *ngIf="generationResult() as result" class="prose max-w-none">
@let content = result.generatedMessage.content;
<!-- Add this block -->
@if (getReasoningPart(content); as reasoning) {
<mat-expansion-panel class="mb-3 !shadow-sm border">
<mat-expansion-panel-header>
<mat-panel-title class="!text-sm !font-semibold">
Reasoning
</mat-panel-title>
</mat-expansion-panel-header>
<div class="whitespace-pre-wrap break-words p-4">{{ reasoning.text }}</div>
</mat-expansion-panel>
}
@let displayContent = getNonReasoningParts(content);
<!-- Handle string response for backward compatibility and text-only responses -->
<ng-container *ngIf="isString(generationResponse())">
<pre class="whitespace-pre-wrap">{{ generationResponse() }}</pre>
<ng-container *ngIf="isString(displayContent)">
<pre class="whitespace-pre-wrap">{{ displayContent }}</pre>
</ng-container>
<!-- Handle complex array response (with text, images, etc.) -->
<ng-container *ngIf="isArray(generationResponse())">
<div *ngFor="let part of generationResponse()">
<ng-container *ngIf="isArray(displayContent)">
<div *ngFor="let part of displayContent">
<pre *ngIf="part.type === 'text'" class="whitespace-pre-wrap">{{ part.text }}</pre>
<img *ngIf="part.type === 'image'" [src]="getImageUrl(part)" alt="Generated Image" class="max-w-full h-auto rounded-md my-2">
<!-- NOTE: Other part types like 'file', 'tool-call' are not visually rendered here but could be added. -->
Expand All @@ -332,7 +354,7 @@ <h2 class="text-xl font-semibold">Response</h2>
</div>

<!-- Placeholder -->
<div *ngIf="!generationResponse() && !isGenerating() && !generationError()" class="text-center text-gray-500 py-8">
<div *ngIf="!generationResult() && !isGenerating() && !generationError()" class="text-center text-gray-500 py-8">
<mat-icon class="text-4xl mb-2 opacity-50">bolt</mat-icon>
<p>Click Generate to see a response</p>
</div>
Expand Down
53 changes: 46 additions & 7 deletions frontend/src/app/modules/prompts/form/prompt-form.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,18 @@ import { MatSliderModule } from '@angular/material/slider';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatTooltipModule } from '@angular/material/tooltip';
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import type { CallSettings, FilePartExt, ImagePartExt, LlmInfo, LlmMessage, TextPart, UserContentExt, AssistantContentExt } from '#shared/llm/llm.model';
import type {
AssistantContentExt,
CallSettings,
FilePartExt,
ImagePartExt,
LlmInfo,
LlmMessage,
ReasoningPart,
TextPart,
ToolCallPartExt,
UserContentExt,
} from '#shared/llm/llm.model';
import type { Prompt } from '#shared/prompts/prompts.model';
import { type PromptCreatePayload, PromptGenerateResponseSchemaModel, type PromptSchemaModel, type PromptUpdatePayload } from '#shared/prompts/prompts.schema';
import { LlmService } from '../../llm.service';
Expand Down Expand Up @@ -98,7 +109,7 @@ export class PromptFormComponent implements OnInit, OnDestroy {
isLoading = signal(true);
isSaving = signal(false);
isGenerating = signal(false);
generationResponse = signal<AssistantContentExt | null>(null);
generationResult = signal<PromptGenerateResponseSchemaModel | null>(null);
generationError = signal<string | null>(null);
private destroy$ = new Subject<void>();
private llmsState$ = toObservable(this.llmService.llmsState);
Expand Down Expand Up @@ -418,13 +429,13 @@ export class PromptFormComponent implements OnInit, OnDestroy {
this.cdr.detectChanges();
}

private _convertLlmContentToString(content: UserContentExt | AssistantContentExt | undefined): string {
private _convertLlmContentToString(content: LlmMessage['content'] | undefined): string {
if (typeof content === 'string') {
return content;
}
if (Array.isArray(content)) {
return content
.map((part) => {
.map((part: any) => {
if (part.type === 'text') {
return (part as TextPart).text;
}
Expand All @@ -436,6 +447,19 @@ export class PromptFormComponent implements OnInit, OnDestroy {
const filePart = part as FilePartExt;
return `[File: ${filePart.filename || filePart.mediaType || 'file'}]`;
}
if (part.type === 'reasoning') {
return (part as ReasoningPart).text;
}
if (part.type === 'redacted-reasoning') {
return '[Redacted Reasoning]';
}
if (part.type === 'tool-call') {
const toolCallPart = part as ToolCallPartExt;
return `[Tool Call: ${toolCallPart.toolName}(${JSON.stringify(toolCallPart.input)})]`;
}
if (part.type === 'tool-result') {
return `[Tool Result: ${JSON.stringify(part.output)}]`;
}
// Fallback for any other part types that might appear in UserContentExt if extended
// Safely access .type, provide a default if it's not a known structure
const partType = (part as any)?.type || 'unknown';
Expand Down Expand Up @@ -630,6 +654,7 @@ export class PromptFormComponent implements OnInit, OnDestroy {

const generationOptions: CallSettings & { llmId?: string } = formValue.options;

this.generationResult.set(null);
this.isGenerating.set(true);
this.generationError.set(null);

Expand All @@ -644,7 +669,7 @@ export class PromptFormComponent implements OnInit, OnDestroy {
)
.subscribe({
next: (response) => {
this.generationResponse.set(response.generatedMessage.content as AssistantContentExt);
this.generationResult.set(response);
this.cdr.detectChanges();
},
error: (error) => {
Expand All @@ -655,7 +680,7 @@ export class PromptFormComponent implements OnInit, OnDestroy {
}

addResponseToPrompt(): void {
const responseContent = this.generationResponse();
const responseContent = this.generationResult()?.generatedMessage.content;
if (!responseContent) {
console.warn('No generated response to add');
return;
Expand All @@ -668,7 +693,7 @@ export class PromptFormComponent implements OnInit, OnDestroy {
messageGroup.get('fullContent')?.setValue(responseContent);
this.messagesFormArray.push(messageGroup);

this.generationResponse.set(null);
this.generationResult.set(null);
this.generationError.set(null);

this.cdr.detectChanges();
Expand Down Expand Up @@ -826,4 +851,18 @@ export class PromptFormComponent implements OnInit, OnDestroy {
}
}
}

public getReasoningPart(content: AssistantContentExt): ReasoningPart | undefined {
if (Array.isArray(content)) {
return content.find((part) => part.type === 'reasoning') as ReasoningPart | undefined;
}
return undefined;
}

public getNonReasoningParts(content: AssistantContentExt): AssistantContentExt {
if (Array.isArray(content)) {
return content.filter((part) => part.type !== 'reasoning');
}
return content;
}
}
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,16 @@
"start": " node -r ts-node/register --env-file=variables/.env src/index.ts",
"start:local": "node -r ts-node/register --env-file=variables/local.env --inspect=0.0.0.0:9229 src/index.ts",
"emulators": "gcloud emulators firestore start --host-port=127.0.0.1:8243",
"test": " npm run test:unit && npm run test:db",
"test": " pnpm run test:unit && pnpm run test:db",
"test:unit": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/**/*.test.[jt]s\" --exclude \"src/modules/{firestore,mongo,postgres}/*.test.ts\" --timeout 10000",
"test:firestore": "node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/firestore/*.test.ts\" --timeout 10000",
"test:postgres": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/postgres/*.test.ts\" --timeout 10000",
"test:mongo": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/mongo/*.test.ts\" --timeout 10000",
"test:db": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" \"src/modules/{firestore,mongo,postgres}/*.test.ts\" --timeout 10000",
"test:single": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r esbuild-register -r \"./src/test/testSetup.ts\" --timeout 10000 --exit",
"test:ci:firestore": "firebase emulators:exec --only firestore \"npm run test:firestore\"",
"test:ci:postgres": " npm run test:postgres",
"test:ci:mongo": " npm run test:mongo",
"test:ci:firestore": "firebase emulators:exec --only firestore \"pnpm run test:firestore\"",
"test:ci:postgres": " pnpm run test:postgres",
"test:ci:mongo": " pnpm run test:mongo",
"test:integration": "node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r ts-node/register -r \"./src/test/testSetup.ts\" \"src/**/*.int.[jt]s\" --timeout 0 --exit",
"test:system": " node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r ts-node/register \"src/**/*.sys.ts\" --timeout 0 --exit",
"test:unit:dev": "TS_NODE_PROJECT='./tsconfig.swc.json' node --env-file=variables/test.env ./node_modules/mocha/bin/mocha -r ts-node/register -t 0 --exit --colors \"src/**/*.test.[jt]s\"",
Expand Down
2 changes: 1 addition & 1 deletion src/agent/agentSerialization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function serializeContext(context: AgentContext): AgentContextApi {
codeTaskId: context.codeTaskId,
state: context.state ?? 'error',
callStack: context.callStack ?? [],
error: context.error ?? undefined,
error: context.error || undefined,
output: context.output,
hilBudget: context.hilBudget ?? 0,
cost: context.cost ?? 0,
Expand Down
4 changes: 2 additions & 2 deletions src/agent/autonomous/functions/agentFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const AGENT_COMPLETED_PARAM_NAME = 'note';
export class Agent {
/**
* Notifies that the user request has completed and there is no more work to be done, or that no more useful progress can be made with the functions.
* @param {string} note A detailed description that answers/completes the user request.
* @param {string} note A detailed description that answers/completes the user request using Markdown formatting.
*/
@func()
async completed(note: string): Promise<void> {
Expand Down Expand Up @@ -60,7 +60,7 @@ export class Agent {
async getMemory(key: string): Promise<string> {
if (!key) throw new Error(`Parameter "key" must be provided. Was ${key}`);
const memory = agentContext().memory;
if (!memory[key]) throw new Error(`Memory key ${key} does not exist`);
if (!memory[key]) throw new Error(`Memory key ${key} does not exist. Valid keys are ${Object.keys(memory).join(', ')}`);
return memory[key];
}

Expand Down
2 changes: 2 additions & 0 deletions src/cli/functionResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { functionRegistry } from '../functionRegistry';
const functionAliases: Record<string, string> = {
f: AgentFeedback.name,
swe: SoftwareDeveloperAgent.name,
bash: CommandLineInterface.name,
shell: CommandLineInterface.name,
cli: CommandLineInterface.name,
code: CodeEditingAgent.name,
query: CodeFunctions.name,
Expand Down
31 changes: 31 additions & 0 deletions src/cli/terminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// file: write-terminal.mjs
import { createWriteStream } from 'node:fs';
import { platform } from 'node:process';

/**
* Writes a message directly to the controlling terminal, bypassing stdout/stderr redirection.
* This is useful for progress indicators, password prompts, or critical alerts
* that should always be visible to the user, even if they pipe the script's output to a file.
*
* @param {string} message The message to write to the terminal.
*/
export function terminalLog(message: string): void {
// Determine the correct path to the terminal device based on the OS.
const terminalPath = platform === 'win32' ? '\\\\.\\CON' : '/dev/tty';

try {
// Create a writable stream to the terminal device.
// This will fail if the process is not running in an interactive terminal
// (e.g., in a CI/CD pipeline, a cron job, or a non-interactive SSH session).
const terminalStream = createWriteStream(terminalPath, {
flags: 'a', // 'a' for append mode is safest
});

terminalStream.write(`${message}\n`);
terminalStream.end(); // Close the stream to release the file handle.
} catch (error) {
// If we can't write to the terminal (e.g., not in a TTY),
// we can fall back to stderr as a last resort for visibility.
// console.error(`Fallback: Could not write directly to terminal. Message: ${message}`);
}
}
19 changes: 11 additions & 8 deletions src/functions/commandLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ export class CommandLineInterface {
Current directory: ${fss.getWorkingDirectory()}
Git repo folder: ${fss.getVcsRoot() ?? '<none>'}
`;
const response = await llms().medium.generateText([
system(
'You are to analyze the provided shell command to determine if it is safe to run, i.e. will not cause configuration changes, data loss or other unintended consequences to the host system or remote systems. Reading files/config and modifying files under version control is acceptable.',
),
user(
`The command which is being requested to execute is:\n${command}\n\n\n Think through the dangers of running this command and response with only a single word, either SAFE, UNSURE or DANGEROUS`,
),
]);
const response = await llms().medium.generateText(
[
system(
'You are to analyze the provided shell command to determine if it is safe to run, i.e. will not cause configuration changes, data loss or other unintended consequences to the host system or remote systems. Reading files/config and modifying files under version control is acceptable.',
),
user(
`The command which is being requested to execute is:\n${command}\n\n\n Think through the dangers of running this command and response with only a single word, either SAFE, UNSURE or DANGEROUS`,
),
],
{ id: 'CLI command safety analysis' },
);
if (!response.includes('SAFE')) {
await humanInTheLoop(
agentContext(),
Expand Down
8 changes: 8 additions & 0 deletions src/llm/services/ai-llm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ export abstract class AiLLM<Provider extends ProviderV2> extends BaseLLM {
// Fallback for unknown parts, though ideally all are handled
return part as any;
}) as Exclude<CoreContent, string>;

// If there are multiple text parts, then concatenate them as some providers don't handle multiple text parts
const textParts = processedContent.filter((part) => part.type === 'text');
if (textParts.length > 1) {
const nonTextParts = processedContent.filter((part) => part.type !== 'text');
const text = textParts.map((part) => part.text).join('\n');
processedContent = [{ type: 'text', text }, ...nonTextParts] as CoreContent;
}
}
return { ...restOfMsg, content: processedContent } as CoreMessage;
});
Expand Down
4 changes: 2 additions & 2 deletions src/llm/services/cerebras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export const CEREBRAS_SERVICE = 'cerebras';
export function cerebrasLLMRegistry(): Record<string, () => LLM> {
return {
'cerebras:qwen-3-32b': () => cerebrasQwen3_32b(),
'cerebras:qwen-3-235b-instruct-2507': () => cerebrasQwen3_235b_Instruct(),
'cerebras:qwen-3-235b-thinking-2507': () => cerebrasQwen3_235b_Thinking(),
'cerebras:qwen-3-235b-a22b-instruct-2507': () => cerebrasQwen3_235b_Instruct(),
'cerebras:qwen-3-235b-a22b-thinking-2507': () => cerebrasQwen3_235b_Thinking(),
'cerebras:qwen-3-coder-480b': () => cerebrasQwen3_Coder(),
'cerebras:llama-4-maverick-17b-128e-instruct': () => cerebrasLlamaMaverick(),
};
Expand Down
Loading
Loading